Map を走査する
Map を走査する とは
Map をキーと値のペアでループする方法を学ぼう。entrySet / keySet / values の使い分けと、順序を保証する LinkedHashMap の使い方まで身につける。
Map をループするのは「リストと少し違う」
List や配列の走査は for-each でサクッと書けますが、Map はキーと値の 2 つを同時に扱うため、ループの書き方にコツがあります。Java の Map は key と value のペアを大量に格納するデータ構造で、辞書のような使い方ができます。たとえば商品名と単価、ユーザー ID と名前、URL とアクセス数などをまとめて持ちたいときに便利です。
Listは「1 列のデータ」、Mapは「2 列のデータ」と覚えると整理しやすいです。Mapを走査するときは「キーの列だけ見たいのか」「値の列だけ見たいのか」「ペアでまとめて見たいのか」を最初に決めましょう。
このレッスンでは、HashMap LinkedHashMap TreeMap の違いをおさえつつ、entrySet keySet values の 3 種類の走査と forEach のラムダ版までを一気に身につけます。最後の課題では、LinkedHashMap<String,Integer> に商品と価格を順番に入れて、合計金額 380 を計算します。
Map 走査の 3 種類
Map をループする方法は、大きく分けて次の通りです。
map.entrySet()を回す — キーと値の両方を同時に取れる、もっとも一般的なやり方map.keySet()を回す — キーだけ欲しいときに使うmap.values()を回す — 値だけ欲しいときに使う
値の合計を出したいだけなら values()、キーの一覧を出したいだけなら keySet()、両方使う処理なら entrySet() が定番です。entrySet を使うときに登場するのが Map.Entry<K,V> という型で、これが「ペア 1 つ分」を表します。
Java
import java.util.HashMap;
import java.util.Map;
public class MapIterateExample {
public static void main(String[] args) {
Map<String, Integer> prices = new HashMap<>();
prices.put("apple", 100);
prices.put("banana", 80);
for (Map.Entry<String, Integer> e : prices.entrySet()) {
System.out.println(e.getKey() + " = " + e.getValue());
}
}
}Map.Entry には getKey() と getValue() の 2 つのメソッドがあり、これを呼ぶとペアの中身が取り出せます。for-each の右側に map.entrySet() を書くだけで、Map を 1 ペアずつ取り出すループが完成します。
entrySet()は 1 度だけ呼ぶ のがコツです。ループ内で何度も呼ぶ必要はありません。for-eachの右側で 1 回だけ呼んでおけば、Map.Entryが次々に流れてきます。
Map の種類で「順序」が変わる
ここがこのレッスンの一番大事なところです。HashMap LinkedHashMap TreeMap は、同じ Map インターフェースですが、走査したときの順序がそれぞれ違います。
HashMap— 順序は不定。putした順番は保証されない。速いが順序が必要な場面では使わないLinkedHashMap— 挿入した順序を保証する。順番が大事な場面の定番TreeMap— キーのソート順に並ぶ。アルファベット順や数値順で取りたいときに使う
たとえば HashMap<String,Integer> に apple banana cherry を順に put しても、走査すると banana, apple, cherry のような順番で出てくる可能性があります。これは内部のハッシュ表のレイアウトで決まるので、自分でコントロールできません。Java 8 以降は条件付きで挿入順に近い順番になる場合もありますが、順序に依存したコードを書いてはいけない のがルールです。
Java
import java.util.LinkedHashMap;
import java.util.Map;
public class OrderedMap {
public static void main(String[] args) {
Map<String, Integer> prices = new LinkedHashMap<>();
prices.put("apple", 100);
prices.put("banana", 80);
prices.put("cherry", 200);
for (Map.Entry<String, Integer> e : prices.entrySet()) {
System.out.println(e.getKey() + " = " + e.getValue());
}
}
}このコードは必ず apple → banana → cherry の順で出力されます。表示順や合計の計算順序を put した順に揃えたいなら、迷わず LinkedHashMap を選びましょう。
「順序が安定しないと困る」「結果を毎回同じにしたい」場合は
LinkedHashMap、「キーで並べたい」場合はTreeMapを使います。HashMapは順序を気にしない場面の最速オプションだと覚えましょう。
entrySet 走査の流れを図で確認
entrySet 走査の流れを Mermaid のフローチャートにしてみました。Map を走査するとき、頭の中ではこんな処理が動いています。
ポイントは、entrySet が その場で新しい Set を作っているわけではない ということです。内部では Map の中身を直接見るビュー (view) を返しているだけなので、ループ中に map.remove(e.getKey()) のような変更をすると ConcurrentModificationException が飛ぶことがあります。走査と変更を同時にやりたい場合は Iterator.remove() を使うか、削除対象を別リストに溜めてからループ後にまとめて消すのが安全です。
Java 8 の forEach でラムダ走査
Java 8 以降は、forEach(BiConsumer) というもっと短い書き方も用意されています。BiConsumer は「引数 2 つを受け取って何もしない関数」を表すインターフェースで、Map の走査と相性ばっちりです。
Java
Map<String, Integer> prices = new LinkedHashMap<>();
prices.put("apple", 100);
prices.put("banana", 80);
prices.put("cherry", 200);
int[] sumHolder = {0};
prices.forEach((key, value) -> {
sumHolder[0] += value;
});
System.out.println(sumHolder[0]);ラムダの中からは外側のローカル変数を「実質 final」でしか参照できないため、合計を入れる箱として int[] sumHolder = {0} のように長さ 1 の配列を使うのが定番テクニックです。シンプルに for-each + entrySet で書けるならそれで十分ですが、forEach(BiConsumer) を覚えておくと、Map の中身を一気にログ出力するときなどに重宝します。
ラムダ版
forEachは短くて読みやすい反面、breakやcontinueが使えません。途中で抜けたい・条件で飛ばしたい場合は、伝統的なfor-each+entrySetの方が向いています。
よくある間違い
Map の走査でつまずきやすいポイントは次の通りです。
HashMapの順序を期待してしまう —putした順やkeyのソート順が出てくると思い込むのは典型的なバグです。順序が必要ならLinkedHashMapかTreeMapを選ぶMap.Entryをnullチェックしてしまう —for-eachが回しているMap.Entry自体はnullにはなりません。nullチェックすべきは中身のvalue(e.getValue() == null) であって、Entryそのものではありませんmap.entrySet()の戻り型を勘違いする —entrySet()はSet<Map.Entry<K,V>>を返します。ListでもMapでもなくSetなので、インデックスでアクセスはできません。あくまで「ペアの集合」をループするためのものだと覚えましょう
もうひとつ、Map.Entry の型宣言を省略してしまうのも初学者にありがちなミスです。for (var e : map.entrySet()) のように var を使えば省略できますが、Map.Entry を明示的に書いた方が補完が効きやすく、レビューでも読みやすくなります。チームのスタイルに合わせて選びましょう。
やってみよう
右側のエディタで、次の手順で課題を解いてみてください。
Solution.totalValue()の中でjava.util.LinkedHashMap<String,Integer>を作るmap.put("apple", 100)map.put("banana", 80)map.put("cherry", 200)の順に値を入れるint sum = 0;を用意し、map.entrySet()をfor-eachで回してsum += e.getValue();する- ループが終わったら
return sum;で合計を返す
テストでは戻り値が 380 であることだけ確認しますが、順序を保証する ために LinkedHashMap を選んでいるところがこのレッスンの一番のポイントです。HashMap でも 100 + 80 + 200 の合計はたまたま同じ 380 になりますが、本番のコードでは「順序が必要なら順序を保証する Map を選ぶ」という意識を必ず持ってください。
慣れてきたら、forEach(BiConsumer) 版にリファクタしたり、keySet() + map.get(key) 版に書き換えたり、values() だけを回す版で書き直したりして、3 種類の走査をすべて手で打ってみると一気に身につきます。Map の走査を自由に書けるようになると、Java の中級ステージはぐっと近づきます。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は Set で重複を排除 で、Set で重複を排除 を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- Map 走査 の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Map 走査 とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- クラス名は
Solution、メソッド名はtotalValue(引数なし、戻り値int) にすること java.util.LinkedHashMap<String,Integer>を使い、apple=100banana=80cherry=200の順にputすることmap.entrySet()をfor-eachで走査し、e.getValue()をsumに足し合わせてreturnすること
入出力例
test-cases.txt
totalValue() → 380