Java 逆引き集 | Collectors.toMap の衝突解決(mergeFunction) — Map 化での安全対策

Java Java
スポンサーリンク

Collectors.toMap の衝突解決(mergeFunction) — Map 化での安全対策

Stream を Map に変換するとき、キー重複があると例外になります。Collectors.toMap の「第3引数 mergeFunction」を使えば、重複キーに遭遇したときの「値のマージ方法」を自分で決められます。初心者がつまずきやすいポイントを、例題とテンプレートでかみ砕いて説明します。


基本の使い方(2引数と3引数の違い)

  • 2引数版(衝突時は例外):
// キー・値の取り出し関数だけ
Map<K, V> map = list.stream()
    .collect(Collectors.toMap(
        item -> key(item),   // keyMapper
        item -> value(item)  // valueMapper
    )); // 重複キーがあると IllegalStateException
Java
  • 3引数版(衝突時にマージ):
Map<K, V> map = list.stream()
    .collect(Collectors.toMap(
        item -> key(item),                      // keyMapper
        item -> value(item),                    // valueMapper
        (oldVal, newVal) -> merge(oldVal, newVal) // mergeFunction
    ));
Java
  • ポイント: 3引数版なら、重複キーに出会った瞬間に (oldVal, newVal) で値をどう統合するか定義できます。

よくあるマージ戦略(例題)

例題1: 後勝ち(新しい値で上書き)

record User(int id, String name) {}
List<User> users = List.of(
    new User(1, "Tanaka"),
    new User(1, "Sato") // 同じ id が重複
);

Map<Integer, String> idToName = users.stream()
    .collect(Collectors.toMap(
        User::id,
        User::name,
        (oldName, newName) -> newName // 後勝ち
    ));

System.out.println(idToName); // {1=Sato}
Java
  • ラベル: 直近のデータを優先したいときに便利。

例題2: 先勝ち(最初の値を保持)

Map<Integer, String> idToName = users.stream()
    .collect(Collectors.toMap(
        User::id,
        User::name,
        (oldName, newName) -> oldName // 先勝ち
    ));
Java
  • ラベル: 先に見つかったレコードを正としたい場合。

例題3: 集計(合計・加算)

record Order(String sku, int qty) {}
List<Order> orders = List.of(
    new Order("A", 3),
    new Order("A", 5),
    new Order("B", 2)
);

Map<String, Integer> qtyBySku = orders.stream()
    .collect(Collectors.toMap(
        Order::sku,
        Order::qty,
        Integer::sum // old + new
    ));

System.out.println(qtyBySku); // {A=8, B=2}
Java
  • ラベル: 数値の重複は Integer::sum(a,b)->a+b が定番。

例題4: 文字列の結合(重複値を並べる)

record Tag(String key, String value) {}
List<Tag> tags = List.of(
    new Tag("env", "dev"),
    new Tag("env", "test"),
    new Tag("role", "api")
);

Map<String, String> tagMap = tags.stream()
    .collect(Collectors.toMap(
        Tag::key,
        Tag::value,
        (a, b) -> a + "," + b // 連結
    ));

System.out.println(tagMap); // {env=dev,test, role=api}
Java
  • ラベル: CSV 連結や追記に向く。ただし値が増えるほど長くなる点に注意。

例題5: 複合オブジェクトの統合(フィールド単位でマージ)

record Stock(String sku, int qty, double price) {}
List<Stock> stocks = List.of(
    new Stock("A", 3, 100.0),
    new Stock("A", 2, 110.0)
);

Map<String, Stock> bySku = stocks.stream()
    .collect(Collectors.toMap(
        Stock::sku,
        s -> s,
        (oldS, newS) -> new Stock(
            oldS.sku(),
            oldS.qty() + newS.qty(),              // 数量は合算
            Math.max(oldS.price(), newS.price())  // 価格は最大を採用 例
        )
    ));
Java
  • ラベル: ドメインルールに従って「どう統合するか」を設計するのがポイント。

Map 実装の指定(順序維持・ソート)

  • 4引数版(Map サプライヤを指定):
Map<K, V> map = list.stream()
    .collect(Collectors.toMap(
        item -> key(item),
        item -> value(item),
        (oldVal, newVal) -> merge(oldVal, newVal),
        LinkedHashMap::new // 格納順を維持
    ));
Java
  • ラベル: 表示順を保ちたいなら LinkedHashMap、キー順が必要なら TreeMap::new を選ぶ。

テンプレート集(そのまま使える)

  • 後勝ち(上書き)
.collect(Collectors.toMap(km, vm, (oldV, newV) -> newV));
Java
  • 先勝ち(保持)
.collect(Collectors.toMap(km, vm, (oldV, newV) -> oldV));
Java
  • 合算(数値)
.collect(Collectors.toMap(km, vm, Integer::sum));
Java
  • 連結(文字列)
.collect(Collectors.toMap(km, vm, (a, b) -> a + "," + b));
Java
  • 順序維持の Map で収集
.collect(Collectors.toMap(km, vm, (a,b)->b, LinkedHashMap::new));
Java
  • 値をリスト化したい(重複を全部保持するなら groupingBy)
.collect(Collectors.groupingBy(km, Collectors.mapping(vm, Collectors.toList())));
Java

実務のコツと落とし穴

  • キー重複で例外:
    • ラベル: 2引数版は重複で IllegalStateException。重複の可能性があるなら3引数版を使い、マージを定義する。
  • null 禁止:
    • ラベル: toMap はデフォルトで null キーや null 値を許容しない。必要なら事前に filter(Objects::nonNull) や代替値を用意。
  • 重複をすべて保持したい:
    • ラベル: 値を失いたくないなら groupingBy(key, mapping(value, toList())) を選ぶ(重複値が List に入る)。
  • 順序・ソートの要件:
    • ラベル: 出力順が重要なら LinkedHashMap、キーソートが必要なら TreeMap を 4引数版で指定。
  • Map.merge との比較:
    • ラベル: 既存の Map に対して後から統合するなら map.merge(k, v, (oldV, newV) -> ... ) も有効。toMap は「新規作成」、merge は「既存更新」。
  • パフォーマンスとメモリ:
    • ラベル: 連結や巨大合算はサイズ増に注意。必要なら文字列は joining で後段生成、数値は summarizing など代替を検討。

まとめ

  • Collectors.toMap は「キー重複あり/なし」で使い分け。重複があり得るなら3引数版でマージ戦略を明示し、順序やソートが必要なら4引数版で Map 実装を指定すると安全。
  • マージは「後勝ち・先勝ち・合算・連結・ドメイン統合」のいずれかを意図で選ぶ。重複を保持したいときは groupingBy が適任。

👉 練習課題: 商品リストから sku -> 合計数量 の Map を作成し、次に LinkedHashMap で「入力順を保持」したバージョンも作ってみてください。マージ関数と Map サプライヤの使い分けが身につきます。

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