List→Map変換は「一覧を“引ける辞書”に変える」技
List<T> は「順番付きのただの並び」です。Map<K, V> は「キーから値を素早く引ける辞書」です。
業務では、DB から List<User> を取ってきたあと、
「ユーザーIDから一発で引きたい」「コードから対応する情報をすぐ取りたい」
という場面がとても多いです。
そのときに使うのが「List→Map変換」です。
“ただの一覧”を、“キーで引ける辞書”に変えるイメージを持ってください。
一番基本:ID をキーにして Map にする
手書き for 文での変換
まずは、Stream を使わない素朴な書き方から。
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class User {
private final String id;
private final String name;
User(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() { return id; }
public String getName() { return name; }
}
public class ListToMapBasic {
public static void main(String[] args) {
List<User> users = List.of(
new User("u001", "山田"),
new User("u002", "佐藤"),
new User("u003", "鈴木")
);
Map<String, User> userMap = new HashMap<>();
for (User u : users) {
userMap.put(u.getId(), u);
}
User yamada = userMap.get("u001");
System.out.println(yamada.getName()); // 山田
}
}
Javaここでの重要ポイントは二つです。
一つ目は、「List<User> の各要素から getId() を取り出し、それをキーにして Map<String, User> を作っている」ことです。
これで「ID → User」という辞書ができ、get("u001") で一発で引けるようになります。
二つ目は、「List の順番は捨てて、“キーで引けること”を優先している」ことです。
List は「順番でアクセス」、Map は「キーでアクセス」という役割の違いを意識してください。
Stream を使った List→Map変換
Collectors.toMap の基本形
Java 8 以降なら、Stream と Collectors.toMap でスッキリ書けます。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Map<String, User> userMap =
users.stream()
.collect(Collectors.toMap(
User::getId, // キーの取り方
u -> u // 値の取り方
));
Javaここでの重要ポイントは三つです。
一つ目は、「toMap(キー関数, 値関数) という形で、“どうやってキーを作るか”“何を値にするか”を指定している」ことです。User::getId が「User からキーを取り出す関数」、u -> u が「User 自身を値にする関数」です。
二つ目は、「u -> u の代わりに User::getName などを渡せば、“ID → 名前”の Map も簡単に作れる」ことです。
Map<String, String> idToName =
users.stream()
.collect(Collectors.toMap(
User::getId,
User::getName
));
Java三つ目は、「toMap は“キーが重複すると例外になる”」ことです。
ここが実務で一番ハマりやすいポイントなので、次で深掘りします。
重要ポイント:キーが重複したらどうするか
そのまま toMap だと Duplicate key 例外
次のような List を考えます。
List<User> users = List.of(
new User("u001", "山田"),
new User("u001", "山田(別)")
);
JavagetId() が同じ "u001" の要素が二つあります。
これをそのまま toMap(User::getId, u -> u) すると、java.lang.IllegalStateException: Duplicate key が発生します。
業務では、「キーが重複する可能性があるか?」を必ず考える必要があります。
重複しない前提なら、「重複したら例外で気づける」のはむしろ安全です。
「重複するかもしれないし、どちらか一つを採用すればいい」なら、マージルールを指定します。
マージ関数付き toMap で「重複時のルール」を決める
Collectors.toMap には、第三引数に「マージ関数」を渡せるオーバーロードがあります。
Map<String, User> userMap =
users.stream()
.collect(Collectors.toMap(
User::getId,
u -> u,
(u1, u2) -> u1 // 同じキーが来たら、先勝ちにする
));
Javaここでの (u1, u2) -> u1 が、「同じキーに対して二つの値が来たとき、どちらを採用するか」を決める関数です。
先勝ちにする (u1, u2) -> u1。
後勝ちにする (u1, u2) -> u2。
どちらにするかは業務ルール次第ですが、
「重複したらどうするか」をコードで明示しておくことがとても大事です。
ユーティリティ化して「毎回同じパターン」を隠す
List→Map変換ユーティリティの例
毎回 stream().collect(toMap(...)) と書くのは少しうるさいので、
ユーティリティにまとめてしまうと読みやすくなります。
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public final class ListMaps {
private ListMaps() {}
public static <T, K> Map<K, T> toMap(
List<T> list,
Function<? super T, ? extends K> keyExtractor
) {
if (list == null || list.isEmpty()) {
return Map.of();
}
return list.stream()
.collect(Collectors.toMap(
keyExtractor,
t -> t
));
}
public static <T, K, V> Map<K, V> toMap(
List<T> list,
Function<? super T, ? extends K> keyExtractor,
Function<? super T, ? extends V> valueExtractor
) {
if (list == null || list.isEmpty()) {
return Map.of();
}
return list.stream()
.collect(Collectors.toMap(
keyExtractor,
valueExtractor
));
}
}
Java使い方はこうです。
Map<String, User> userMap =
ListMaps.toMap(users, User::getId);
Map<String, String> idToName =
ListMaps.toMap(users, User::getId, User::getName);
Javaここでの重要ポイントは三つです。
一つ目は、「null や空 List のときに空 Map を返す」ことです。
呼び出し側は「とりあえず Map が欲しい」とだけ考えればよく、null チェックを毎回書かなくて済みます。
二つ目は、「キーの取り方・値の取り方をラムダで渡せる」ことです。User::getId や User::getName のように、業務ルールをそのまま書けます。
三つ目は、「“List→Map変換”というよくあるパターンを名前付きの関数にして、意図をはっきりさせている」ことです。ListMaps.toMap(users, User::getId) と書けば、「ID をキーにした Map を作っているんだな」と一目で分かります。
グルーピングとの違いに注意する
「1キー1値」と「1キー多値」は別物
List→Map変換と似たものに、「グルーピング(groupingBy)」があります。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Map<String, List<User>> usersByDept =
users.stream()
.collect(Collectors.groupingBy(User::getDepartment));
Javaこれは「部署 → その部署のユーザー一覧」という Map<String, List<User>> を作ります。
一方、今回の List→Map変換は「1キー1値」が前提です。Map<String, User> のように、「そのキーに対応するのは一つの値だけ」という設計です。
ここでの重要ポイントは、
「“キーごとに一つだけ欲しい”のか、“キーごとに全部欲しい”のかを最初に決める」ことです。
一つだけなら toMap。
全部なら groupingBy。
この切り分けを意識しておくと、List→Map変換を使う場面がクリアになります。
まとめ:List→Map変換ユーティリティで身につけてほしい感覚
List→Map変換は、
単に「Stream の書き方を覚える」話ではなく、
「“ただの一覧”を、“キーで引ける辞書”に変える設計」です。
List<T> から「何をキーにするか」「何を値にするか」をはっきり決める。Collectors.toMap で、「キー関数」「値関数」「(必要なら)マージ関数」を指定して変換する。
キー重複時にどうするか(例外にする/先勝ち/後勝ち)を、業務ルールとして決めてコードに刻む。
よく使うパターンは ListMaps.toMap のようなユーティリティにまとめて、呼び出し側の意図をシンプルに書く。
あなたのコードのどこかに、
「List を毎回線形検索して ID で探している」箇所があれば、
それを一度「List→Map変換+Map#get」に置き換えられないか眺めてみてください。
その小さな変換が、
「データ構造を“引き方”から設計できるエンジニア」への、
確かな一歩になります。
