Map を走査する

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

Map を走査する とは

Map をキーと値のペアでループする方法を学ぼう。entrySet / keySet / values の使い分けと、順序を保証する LinkedHashMap の使い方まで身につける。

Map をループするのは「リストと少し違う」

List や配列の走査は for-each でサクッと書けますが、Map はキーと値の 2 つを同時に扱うため、ループの書き方にコツがあります。Java の Mapkeyvalue のペアを大量に格納するデータ構造で、辞書のような使い方ができます。たとえば商品名と単価、ユーザー 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()); } } }

このコードは必ず applebananacherry の順で出力されます。表示順や合計の計算順序を put した順に揃えたいなら、迷わず LinkedHashMap を選びましょう。

「順序が安定しないと困る」「結果を毎回同じにしたい」場合は LinkedHashMap、「キーで並べたい」場合は TreeMap を使います。HashMap は順序を気にしない場面の最速オプションだと覚えましょう。

entrySet 走査の流れを図で確認

entrySet 走査の流れを Mermaid のフローチャートにしてみました。Map を走査するとき、頭の中ではこんな処理が動いています。

diagram (will load when visible)

ポイントは、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 は短くて読みやすい反面、breakcontinue が使えません。途中で抜けたい・条件で飛ばしたい場合は、伝統的な for-each + entrySet の方が向いています。

よくある間違い

Map の走査でつまずきやすいポイントは次の通りです。

  • HashMap の順序を期待してしまうput した順や key のソート順が出てくると思い込むのは典型的なバグです。順序が必要なら LinkedHashMapTreeMap を選ぶ
  • Map.Entrynull チェックしてしまう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 を明示的に書いた方が補完が効きやすく、レビューでも読みやすくなります。チームのスタイルに合わせて選びましょう。

やってみよう

右側のエディタで、次の手順で課題を解いてみてください。

  1. Solution.totalValue() の中で java.util.LinkedHashMap<String,Integer> を作る
  2. map.put("apple", 100) map.put("banana", 80) map.put("cherry", 200) の順に値を入れる
  3. int sum = 0; を用意し、map.entrySet()for-each で回して sum += e.getValue(); する
  4. ループが終わったら 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 つができることを確認しましょう。

  1. Map 走査 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. Map 走査 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. クラス名は Solution、メソッド名は totalValue (引数なし、戻り値 int) にすること
  2. java.util.LinkedHashMap<String,Integer> を使い、apple=100 banana=80 cherry=200 の順に put すること
  3. map.entrySet()for-each で走査し、e.getValue()sum に足し合わせて return すること

入出力例

test-cases.txt

totalValue()380

ヒント

main.java
main.java
学習モード

メモ

Map を走査する

⌘S で保存