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引数版を使い、マージを定義する。
- ラベル: 2引数版は重複で
- 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 は「既存更新」。
- ラベル: 既存の Map に対して後から統合するなら
- パフォーマンスとメモリ:
- ラベル: 連結や巨大合算はサイズ増に注意。必要なら文字列は
joiningで後段生成、数値は summarizing など代替を検討。
- ラベル: 連結や巨大合算はサイズ増に注意。必要なら文字列は
まとめ
Collectors.toMapは「キー重複あり/なし」で使い分け。重複があり得るなら3引数版でマージ戦略を明示し、順序やソートが必要なら4引数版で Map 実装を指定すると安全。- マージは「後勝ち・先勝ち・合算・連結・ドメイン統合」のいずれかを意図で選ぶ。重複を保持したいときは
groupingByが適任。
👉 練習課題: 商品リストから sku -> 合計数量 の Map を作成し、次に LinkedHashMap で「入力順を保持」したバージョンも作ってみてください。マージ関数と Map サプライヤの使い分けが身につきます。
