Java Tips | コレクション:List→Map変換

Java Java
スポンサーリンク

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", "山田(別)")
);
Java

getId() が同じ "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::getIdUser::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」に置き換えられないか眺めてみてください。

その小さな変換が、
「データ構造を“引き方”から設計できるエンジニア」への、
確かな一歩になります。

タイトルとURLをコピーしました