- JAVA
Java ListをMapに変換する方法 toMapのキー重複エラー対処
JavaでListをMapに変換するには、Stream APIの collect に Collectors.toMap(キーを作る関数, 値を作る関数)を渡します。 キーが重なる可能性があるときは3つ目の引数(マージ関数)で「どちらを残すか」を決め、順序を保ちたいときは4つ目の引数で LinkedHashMap を指定します。1つのキーに複数の要素をまとめたいときは groupingBy が向いています。
この記事では、toMap の2引数・3引数・4引数と groupingBy・partitioningBy を「結果の型/キー重複時/順序」で比べる早見表を最初に示します。そのうえで、Duplicate key の IllegalStateException と、値が null のときの NullPointerException をスタックトレースごと再現し、先勝ち・後勝ち・合算・リスト化の4つの直し方を実行結果つきで並べます。コードはすべて1ファイルで動く完成形です。
確認環境:Ubuntu 24.04、OpenJDK 21.0.10(java ファイル名.java で実行)。エラー文の比較だけ、OpenJDK 8(1.8.0_504)と OpenJDK 11.0.32 でも同じコードを実行しました。
ListからMapへの変換 早見表
| 書き方 | 結果の型 | キーが重複したとき | 順序 | 向いている場面 |
|---|---|---|---|---|
toMap(キー, 値)(2引数) |
Map<K, V>(Java 21では HashMap) |
IllegalStateException で止まる | 保たれない | キーが必ず一意(IDなど) |
toMap(キー, 値, マージ関数)(3引数) |
Map<K, V>(Java 21では HashMap) |
マージ関数の戻り値を採用 | 保たれない | 先勝ち・後勝ち・合算を決めたい |
toMap(キー, 値, マージ関数, Mapの作り方)(4引数) |
指定したMap(LinkedHashMap、TreeMapなど) | マージ関数の戻り値を採用 | LinkedHashMapなら出てきた順、TreeMapならキーの昇順 | 順序を保ちたい・並べたい |
groupingBy(キー) |
Map<K, List<T>> |
同じキーの要素をリストにまとめる | 保たれない(3引数版でLinkedHashMapを指定可) | 1つのキーに複数の要素 |
groupingBy(キー, 集計) |
Map<K, 集計結果> |
件数・合計などに集計 | 同上 | 部署ごとの件数・合計 |
partitioningBy(条件) |
Map<Boolean, List<T>> |
true と false の2つに振り分け | false、true の順 | 条件に合う/合わないで分ける |
toUnmodifiableMap(キー, 値)(Java 10以降) |
変更できないMap | IllegalStateException で止まる | 保たれない | 作ったあとに変更させたくない |
for文と put |
自分で用意したMap | 後から入れた値で上書き(後勝ち) | 用意したMapしだい | 値に null を入れたい・処理が複雑 |
| つまずきやすい点 | 起きること | 直し方 |
|---|---|---|
| 2引数の toMap でキーが重複 | IllegalStateException: Duplicate key ... |
マージ関数を足す、または groupingBy にする |
| 値を作る関数が null を返す | NullPointerException(引数の数に関係なく発生) |
既定値に置き換える・除外する・put で入れる |
| groupingBy のキーが null | NullPointerException: element cannot be mapped to a null key |
null を別の値に置き換えてからグループ化 |
| 表示順がリストと違う | HashMap は順序を保たない | 4引数で LinkedHashMap::new を渡す |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java ListをMapに変換する方法 toMapのキー重複エラー対処」
基本:toMapでキーと値を指定する
toMap の最初の引数は「要素からキーを作る関数」、2つ目は「要素から値を作る関数」です。record(Java 16以降で使える、データを入れるための短いクラスの書き方)を使うと、Member::id のようなメソッド参照でそのまま書けます。
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Basic {
record Member(int id, String name, String dept, int score) {}
public static void main(String[] args) {
List<Member> members = List.of(
new Member(103, "佐藤", "開発", 80),
new Member(101, "鈴木", "営業", 65),
new Member(102, "高橋", "開発", 92));
// 1. キー=id、値=名前
Map<Integer, String> nameById = members.stream()
.collect(Collectors.toMap(Member::id, Member::name));
System.out.println("1: " + nameById);
// 2. キー=id、値=要素そのもの(Function.identity)
Map<Integer, Member> memberById = members.stream()
.collect(Collectors.toMap(Member::id, Function.identity()));
System.out.println("2: " + memberById.get(102));
// 3. 返ってくるMapの実装クラス
System.out.println("3: " + nameById.getClass().getName());
// 4. for文で書いた場合
Map<Integer, String> byLoop = new HashMap<>();
for (Member m : members) {
byLoop.put(m.id(), m.name());
}
System.out.println("4: " + byLoop);
}
}
実行結果:
1: {101=鈴木, 102=高橋, 103=佐藤}
2: Member[id=102, name=高橋, dept=開発, score=92]
3: java.util.HashMap
4: {101=鈴木, 102=高橋, 103=佐藤}
読み取れることは3つあります。
- 値に要素そのものを入れたいときは Function.identity() を使います。 これは「受け取ったものをそのまま返す関数」で、
m -> mと書いても同じ結果になります。IDから要素を引く辞書を作るときの定番です。 - 2引数の toMap が返すMapは、Java 21 では java.util.HashMap でした。ただし公式ドキュメントは、返されるMapの型・変更できるかどうか・スレッド安全性について保証しないと書いています。型を決めたいときは4引数版でMapの作り方を渡します(後述)。
- for文と put でも同じMapが作れます。Streamとの違いは、キーが重複したときの動きです。put は後から入れた値で静かに上書きしますが、2引数の toMap は例外で知らせます。
メソッド参照とラムダ式の対応
| 書き方 | 同じ意味のラムダ式 | 用途 |
|---|---|---|
Member::id |
m -> m.id() |
recordの項目をキーにする |
Member::getId |
m -> m.getId() |
getterを持つ通常のクラス |
Function.identity() |
m -> m |
要素そのものを値(またはキー)にする |
String::length |
s -> s.length() |
文字列の長さを値にする |
キー重複のエラーを再現する:IllegalStateException
キーにした項目に同じ値が2回出てくると、2引数の toMap は止まります。部署名をキーにして、同じ部署の人が2人いるリストで試します。
import java.util.*;
import java.util.stream.Collectors;
public class Dup {
record Member(int id, String name, String dept, int score) {}
public static void main(String[] args) {
List<Member> members = List.of(
new Member(101, "鈴木", "営業", 65),
new Member(102, "高橋", "開発", 92),
new Member(103, "佐藤", "開発", 80));
// キーにした部署名「開発」が2件ある
Map<String, String> nameByDept = members.stream()
.collect(Collectors.toMap(Member::dept, Member::name));
System.out.println(nameByDept);
}
}
実行結果(Java 21):
Exception in thread "main" java.lang.IllegalStateException: Duplicate key 開発 (attempted merging values 高橋 and 佐藤)
at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:135)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:182)
at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.base/java.util.AbstractList$RandomAccessSpliterator.forEachRemaining(AbstractList.java:722)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at Dup.main(Dup.java:15)
メッセージの読み方は次のとおりです。
| 部分 | 意味 |
|---|---|
Duplicate key 開発 |
重複したキーは「開発」 |
attempted merging values 高橋 and 佐藤 |
すでに入っていた値「高橋」と、後から来た値「佐藤」をまとめようとした |
at Dup.main(Dup.java:15) |
自分のコードで collect を呼んだ行 |
スタックトレースは下から読むと、自分のコードの行(Dup.java:15)にすぐたどり着けます。 上のほうの java.base/ で始まる行はJDK内部の処理です。
Java 8とJava 9以降でエラー文が違う
同じ内容のコード(部署 dev に takahashi と sato がいるリスト)を、3つのバージョンで実行して例外を表示させました。
import java.util.*;
import java.util.stream.Collectors;
public class Dup8 {
public static void main(String[] args) {
System.out.println("java.version = " + System.getProperty("java.version"));
List<String[]> rows = Arrays.asList(
new String[] {"sales", "suzuki"},
new String[] {"dev", "takahashi"},
new String[] {"dev", "sato"});
try {
rows.stream().collect(Collectors.toMap(r -> r[0], r -> r[1]));
} catch (IllegalStateException e) {
System.out.println(e);
}
}
}
| 実行したJava | 表示されたメッセージ |
|---|---|
| OpenJDK 8(1.8.0_504) | java.lang.IllegalStateException: Duplicate key takahashi |
| OpenJDK 11.0.32 | java.lang.IllegalStateException: Duplicate key dev (attempted merging values takahashi and sato) |
| OpenJDK 21.0.10 | java.lang.IllegalStateException: Duplicate key dev (attempted merging values takahashi and sato) |
Java 8 では「Duplicate key」の後ろに、キーではなく先に入っていた値(takahashi)が表示されます。 キーは dev なので、メッセージだけ見ると戸惑いやすいところです。OpenJDKの課題票 JDK-8040892 に「Duplicate key の後ろに出るのはキーではなく値のひとつ」という報告があり、修正済みになっています。Java 11 と 21 では、キーと2つの値がそろって表示されました。Java 8 で動いているシステムのログを読むときは、「Duplicate key の後ろは値」と覚えておくと原因のキーを探しやすくなります。
キー重複の直し方4つ:先勝ち・後勝ち・合算・リスト化
3つ目の引数(マージ関数)は、同じキーが出てきたときに「すでに入っている値 a」と「新しく来た値 b」を受け取り、残す値を返す関数です。
import java.util.*;
import java.util.stream.Collectors;
public class Fix {
record Member(int id, String name, String dept, int score) {}
public static void main(String[] args) {
List<Member> members = List.of(
new Member(101, "鈴木", "営業", 65),
new Member(102, "高橋", "開発", 92),
new Member(103, "佐藤", "開発", 80));
// 直し方1: 先勝ち(先に出てきた値を残す)
Map<String, String> first = members.stream()
.collect(Collectors.toMap(Member::dept, Member::name, (a, b) -> a));
System.out.println("先勝ち : " + first);
// 直し方2: 後勝ち(後から出てきた値で上書き)
Map<String, String> last = members.stream()
.collect(Collectors.toMap(Member::dept, Member::name, (a, b) -> b));
System.out.println("後勝ち : " + last);
// 直し方3: 合算(同じキーの値を足す)
Map<String, Integer> sum = members.stream()
.collect(Collectors.toMap(Member::dept, Member::score, Integer::sum));
System.out.println("合算 : " + sum);
// 直し方4: リスト化(同じキーの値をすべて残す)
Map<String, List<String>> list = members.stream()
.collect(Collectors.groupingBy(Member::dept,
Collectors.mapping(Member::name, Collectors.toList())));
System.out.println("リスト化: " + list);
// 参考: 文字列としてつなぐ
Map<String, String> joined = members.stream()
.collect(Collectors.toMap(Member::dept, Member::name, (a, b) -> a + "・" + b));
System.out.println("連結 : " + joined);
// 参考: 点数が高いほうを残す
Map<String, Member> top = members.stream()
.collect(Collectors.toMap(Member::dept, m -> m,
(a, b) -> a.score() >= b.score() ? a : b));
System.out.println("最高点 : " + top);
}
}
実行結果:
先勝ち : {開発=高橋, 営業=鈴木}
後勝ち : {開発=佐藤, 営業=鈴木}
合算 : {開発=172, 営業=65}
リスト化: {開発=[高橋, 佐藤], 営業=[鈴木]}
連結 : {開発=高橋・佐藤, 営業=鈴木}
最高点 : {開発=Member[id=102, name=高橋, dept=開発, score=92], 営業=Member[id=101, name=鈴木, dept=営業, score=65]}
| 直し方 | マージ関数・書き方 | 「開発」の結果 | 選ぶ目安 |
|---|---|---|---|
| 先勝ち | (a, b) -> a |
高橋 | 最初に登録されたものを正とする |
| 後勝ち | (a, b) -> b |
佐藤 | 新しいデータで上書きしたい |
| 合算 | Integer::sum |
172 | 数値を合計したい |
| リスト化 | groupingBy と mapping |
[高橋, 佐藤] | すべて残したい |
| 連結 | (a, b) -> a + "・" + b |
高橋・佐藤 | 表示用に1つの文字列にしたい |
| 条件で選ぶ | (a, b) -> a.score() >= b.score() ? a : b |
高橋(92点) | 最大・最新などを残したい |
どれを選ぶかは「重複が起きたとき、業務としてどれが正しいか」で決まります。 重複が起きないはずのデータ(IDなど)は、あえて2引数のままにして例外で気づけるようにするのも1つの考え方です。重複がありえるデータは、先勝ちか後勝ちかを明示しておくと、読む人に意図が伝わります。
toMapでリスト化する書き方との比較
リスト化は toMap でも書けますが、groupingBy のほうが短く読みやすくなります。
| 書き方 | コード |
|---|---|
| groupingBy(要素のリスト) | Collectors.groupingBy(Member::dept) |
| groupingBy(名前だけのリスト) | Collectors.groupingBy(Member::dept, Collectors.mapping(Member::name, Collectors.toList())) |
| toMap で同じことをする | 値を new ArrayList<>(List.of(m.name())) にし、マージ関数で a.addAll(b); return a; と書く |
値がnullのときのNullPointerExceptionを再現する
もう1つつまずきやすいのが、値を作る関数が null を返すケースです。HashMap 自体は値に null を入れられますが、toMap は受け付けません。
import java.util.*;
import java.util.stream.Collectors;
public class NullVal {
record Item(String code, String memo) {}
public static void main(String[] args) {
List<Item> items = List.of(
new Item("A-1", "棚1"),
new Item("A-2", null), // メモが未入力
new Item("A-3", "棚3"));
Map<String, String> memoByCode = items.stream()
.collect(Collectors.toMap(Item::code, Item::memo));
System.out.println(memoByCode);
}
}
実行結果(Java 21):
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.base/java.util.AbstractList$RandomAccessSpliterator.forEachRemaining(AbstractList.java:722)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at NullVal.main(NullVal.java:14)
メッセージ本文がなく、自分のコードの行は最後の NullVal.java:14(collect の行)だけです。スタックトレースに Collectors と requireNonNull が並んでいたら、「toMap の値が null」と見当がつきます。 Java 8 で同じことをすると、例外が出る場所は java.util.HashMap.merge(HashMap.java:1226) でした(Java 11 と 21 は Objects.requireNonNull)。場所は違っても、原因は同じです。
なお、マージ関数を足した3引数・4引数でも、値が null なら同じように止まります。null は引数の数では解決しないので、次のいずれかで直します。
import java.util.*;
import java.util.stream.Collectors;
public class NullFix {
record Item(String code, String memo) {}
public static void main(String[] args) {
List<Item> items = List.of(
new Item("A-1", "棚1"),
new Item("A-2", null),
new Item("A-3", "棚3"));
// 直し方1: nullを既定値に置き換える
Map<String, String> a = items.stream()
.collect(Collectors.toMap(Item::code,
i -> Objects.requireNonNullElse(i.memo(), "")));
System.out.println("既定値 : " + a);
// 直し方2: nullの要素を除いてから集める
Map<String, String> b = items.stream()
.filter(i -> i.memo() != null)
.collect(Collectors.toMap(Item::code, Item::memo));
System.out.println("除外 : " + b);
// 直し方3: nullのまま入れる(3引数のcollectでputを使う)
Map<String, String> c = items.stream()
.collect(HashMap::new, (m, i) -> m.put(i.code(), i.memo()), HashMap::putAll);
System.out.println("nullのまま: " + c);
// 直し方4: for文とputでnullのまま入れる
Map<String, String> d = new LinkedHashMap<>();
for (Item i : items) {
d.put(i.code(), i.memo());
}
System.out.println("for文 : " + d);
}
}
実行結果:
既定値 : {A-1=棚1, A-2=, A-3=棚3}
除外 : {A-1=棚1, A-3=棚3}
nullのまま: {A-1=棚1, A-2=null, A-3=棚3}
for文 : {A-1=棚1, A-2=null, A-3=棚3}
| 直し方 | 結果 | 選ぶ目安 |
|---|---|---|
既定値に置き換える(Objects.requireNonNullElse) |
A-2=(空文字) |
後の処理で null を気にしたくない |
| filter で除く | A-2 がMapに入らない | 値がないものは不要 |
| 3引数の collect で put する | A-2=null |
null のまま持ちたい・Streamで書きたい |
| for文と put | A-2=null |
null のまま持ちたい・順序も決めたい |
キーがnullのとき:toMapとgroupingByで動きが違う
import java.util.*;
import java.util.stream.Collectors;
public class NullKey {
record Member(String name, String dept) {}
public static void main(String[] args) {
List<Member> members = List.of(
new Member("sato", "dev"), new Member("ito", null));
// toMapはキーがnullでも入る(HashMapはnullキーを1つ持てる)
System.out.println(members.stream()
.collect(Collectors.toMap(Member::dept, Member::name)));
// groupingByはキーがnullだと止まる
try {
members.stream().collect(Collectors.groupingBy(Member::dept));
} catch (NullPointerException e) {
System.out.println(e);
}
// 直し方: nullを「未設定」に置き換えてからグループ化
System.out.println(members.stream().collect(Collectors.groupingBy(
m -> Objects.requireNonNullElse(m.dept(), "未設定"),
Collectors.mapping(Member::name, Collectors.toList()))));
}
}
実行結果:
{null=ito, dev=sato}
java.lang.NullPointerException: element cannot be mapped to a null key
{未設定=[ito], dev=[sato]}
toMap はキーが null でもMapに入りました(HashMap は null のキーを1つ持てます)。一方、groupingBy はキーが null だと「element cannot be mapped to a null key」というメッセージで止まります。 項目が空の可能性があるときは、先に「未設定」などの値に置き換えてからグループ化すると、空のデータも1つのグループとして扱えます。
順序を保つ:LinkedHashMapとTreeMapを4引数で指定する
HashMap は入れた順序を保ちません。リストの順序のままMapにしたいときは、4つ目の引数に LinkedHashMap::new を渡します。4引数版は3つ目のマージ関数を省略できないので、重複しない場合でも (a, b) -> a などを書きます。
import java.util.*;
import java.util.stream.Collectors;
public class Order {
record Member(int id, String name, String dept, int score) {}
public static void main(String[] args) {
List<Member> members = List.of(
new Member(3, "sato", "dev", 80),
new Member(1, "suzuki", "sales", 65),
new Member(2, "takahashi", "dev", 92),
new Member(4, "ito", "admin", 58));
// 2引数・3引数: HashMap(リストの順序は残らない)
Map<String, Integer> hash = members.stream()
.collect(Collectors.toMap(Member::name, Member::score));
System.out.println("HashMap : " + hash);
// 4引数: LinkedHashMap(リストの順序のまま)
Map<String, Integer> linked = members.stream()
.collect(Collectors.toMap(Member::name, Member::score,
(a, b) -> a, LinkedHashMap::new));
System.out.println("LinkedHashMap: " + linked);
// 4引数: TreeMap(キーの昇順)
Map<String, Integer> tree = members.stream()
.collect(Collectors.toMap(Member::name, Member::score,
(a, b) -> a, TreeMap::new));
System.out.println("TreeMap : " + tree);
// 先に並べ替えてからLinkedHashMapに入れる(点数の高い順)
Map<String, Integer> sorted = members.stream()
.sorted(Comparator.comparingInt(Member::score).reversed())
.collect(Collectors.toMap(Member::name, Member::score,
(a, b) -> a, LinkedHashMap::new));
System.out.println("点数の高い順 : " + sorted);
}
}
実行結果:
HashMap : {takahashi=92, ito=58, sato=80, suzuki=65}
LinkedHashMap: {sato=80, suzuki=65, takahashi=92, ito=58}
TreeMap : {ito=58, sato=80, suzuki=65, takahashi=92}
点数の高い順 : {takahashi=92, sato=80, suzuki=65, ito=58}
| 指定 | 並び | 用途 |
|---|---|---|
| 指定なし(HashMap) | 入れた順でもキーの順でもない | 順序が不要で、取り出しだけ速ければよい |
LinkedHashMap::new |
リストに出てきた順 | 画面表示・CSV出力など、元の順序を見せたい |
TreeMap::new |
キーの昇順 | キーで並べて見せたい・範囲で取り出したい |
sorted の後に LinkedHashMap::new |
並べ替えた順 | 値の大きい順など、キー以外の順序にしたい |
値の順に並んだMapがほしいときは、「先に sorted で並べ替え、LinkedHashMap に入れる」の2段階にします。 TreeMap はキーで並べるMapなので、値の順には使えません。
groupingByとpartitioningBy:1つのキーに複数の要素をまとめる
「部署ごとのメンバー一覧」のように、1つのキーに複数の要素がぶら下がる形がほしいときは groupingBy を使います。条件で2つに分けたいだけなら partitioningBy が簡潔です。
import java.util.*;
import java.util.stream.Collectors;
public class Group {
record Member(int id, String name, String dept, int score) {}
public static void main(String[] args) {
List<Member> members = List.of(
new Member(3, "sato", "dev", 80),
new Member(1, "suzuki", "sales", 65),
new Member(2, "takahashi", "dev", 92),
new Member(4, "ito", "admin", 58));
// groupingBy(1引数): キーごとに要素のリストを作る
Map<String, List<Member>> byDept = members.stream()
.collect(Collectors.groupingBy(Member::dept));
byDept.forEach((k, v) -> System.out.println("groupingBy : " + k + " = "
+ v.stream().map(Member::name).toList()));
System.out.println("実装クラス : " + byDept.getClass().getSimpleName()
+ " / " + byDept.get("dev").getClass().getSimpleName());
// groupingBy(2引数): 件数・合計
System.out.println("件数 : " + members.stream()
.collect(Collectors.groupingBy(Member::dept, Collectors.counting())));
System.out.println("合計 : " + members.stream()
.collect(Collectors.groupingBy(Member::dept, Collectors.summingInt(Member::score))));
// groupingBy(3引数): 出てきた順を保ち、名前だけのリストにする
Map<String, List<String>> ordered = members.stream()
.collect(Collectors.groupingBy(Member::dept, LinkedHashMap::new,
Collectors.mapping(Member::name, Collectors.toList())));
System.out.println("順序を保つ : " + ordered);
// partitioningBy: 条件に合う/合わないの2つに分ける
Map<Boolean, List<String>> pass = members.stream()
.collect(Collectors.partitioningBy(m -> m.score() >= 70,
Collectors.mapping(Member::name, Collectors.toList())));
System.out.println("partitioning: " + pass);
System.out.println("70点以上 : " + pass.get(true));
// 該当が0件でも true と false の両方のキーが入る
Map<Boolean, Long> none = members.stream()
.collect(Collectors.partitioningBy(m -> m.score() >= 100, Collectors.counting()));
System.out.println("該当なしでも: " + none);
}
}
実行結果:
groupingBy : dev = [sato, takahashi]
groupingBy : admin = [ito]
groupingBy : sales = [suzuki]
実装クラス : HashMap / ArrayList
件数 : {dev=2, admin=1, sales=1}
合計 : {dev=172, admin=58, sales=65}
順序を保つ : {dev=[sato, takahashi], sales=[suzuki], admin=[ito]}
partitioning: {false=[suzuki, ito], true=[sato, takahashi]}
70点以上 : [sato, takahashi]
該当なしでも: {false=4, true=0}
- groupingBy(1引数)の結果は、Java 21 では HashMap と ArrayList でした。toMap と同じく、型は保証されていません。順序を決めたいときは3引数版で
LinkedHashMap::newを渡します。 - 2つ目の引数(下流のコレクター)を変えると、リスト以外の形に集計できます。 counting(件数)、summingInt(合計)、mapping(一部の項目だけ取り出す)などです。
- partitioningBy は、該当が0件でも true と false の両方のキーが必ず入ります。 そのため
pass.get(true)が null になる心配がありません。公式ドキュメントにも、返されるMapは常に false と true の両方を含むと書かれています。
toMapとgroupingByのどちらを使うか
flowchart TD
A[ListをMapにしたい] --> B{1つのキーに<br>要素は1つ?}
B -->|はい| C{キーは必ず一意?}
B -->|いいえ 複数ある| D{分け方は?}
C -->|はい| E[toMap 2引数]
C -->|重複がありえる| F[toMap 3引数<br>先勝ち・後勝ち・合算]
D -->|項目の値ごと| G[groupingBy]
D -->|条件で2つ| H[partitioningBy]
E --> I{順序を保つ?}
F --> I
I -->|はい| J[4引数で LinkedHashMap]
I -->|いいえ| K[そのまま]
| ほしい結果 | 使うもの |
|---|---|
| ID → 要素 | toMap(Member::id, Function.identity()) |
| 部署 → メンバーのリスト | groupingBy(Member::dept) |
| 部署 → 人数 | groupingBy(Member::dept, Collectors.counting()) |
| 部署 → 点数の合計 | groupingBy(..., summingInt(...)) または toMap(Member::dept, Member::score, Integer::sum) |
| 部署 → 最高点の1人 | toMap(Member::dept, m -> m, (a, b) -> a.score() >= b.score() ? a : b) |
| 合格/不合格 → リスト | partitioningBy(m -> m.score() >= 70) |
よく使う変換の例:添字・文字列・2つのリスト・変更できないMap
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Misc {
record Member(int id, String name) {}
public static void main(String[] args) {
List<Member> members = List.of(new Member(1, "sato"), new Member(2, "suzuki"));
List<String> names = List.of("sato", "suzuki", "ito");
// 変更できないMapにする(Java 10以降)
Map<Integer, String> fixed = members.stream()
.collect(Collectors.toUnmodifiableMap(Member::id, Member::name));
try {
fixed.put(9, "x");
} catch (UnsupportedOperationException e) {
System.out.println("変更不可 : " + e);
}
// 添字をキーにする
Map<Integer, String> byIndex = IntStream.range(0, names.size())
.boxed()
.collect(Collectors.toMap(Function.identity(), names::get));
System.out.println("添字がキー: " + byIndex);
// 文字列のリスト → 文字列と長さ
Map<String, Integer> length = names.stream()
.collect(Collectors.toMap(Function.identity(), String::length));
System.out.println("長さ : " + length);
// 2つのリスト(キーのリストと値のリスト)を組み合わせる
List<String> keys = List.of("a", "b", "c");
List<Integer> values = List.of(10, 20, 30);
Map<String, Integer> zipped = IntStream.range(0, keys.size())
.boxed()
.collect(Collectors.toMap(keys::get, values::get, (x, y) -> x, LinkedHashMap::new));
System.out.println("2つを結合 : " + zipped);
// 取り出すときは get と getOrDefault
System.out.println("get : " + length.get("sato") + " / " + length.get("abe")
+ " / " + length.getOrDefault("abe", 0));
}
}
実行結果:
変更不可 : java.lang.UnsupportedOperationException
添字がキー: {0=sato, 1=suzuki, 2=ito}
長さ : {ito=3, sato=4, suzuki=6}
2つを結合 : {a=10, b=20, c=30}
get : 4 / null / 0
- toUnmodifiableMap は Java 10 で追加されたコレクターで、変更できないMapを返します。 put を呼ぶと UnsupportedOperationException になります。公式ドキュメントによると、キーにも値にも null を受け付けず、キーが重複すると IllegalStateException になります(マージ関数を渡す3引数版もあります)。
- 添字をキーにしたいときは、
IntStream.range(0, 件数)で 0 からの番号を作り、boxed で Integer に変えてから toMap に渡します。 - キーのリストと値のリストを組み合わせる場合も、同じく添字を使います。2つのリストの件数が違うと get で範囲外になるため、件数をそろえてから使います。
- 作ったMapから値を取り出すとき、キーがなければ get は null を返します。getOrDefault を使うと、キーがないときの値を決められます。
チェックリスト:toMapを書くときに確かめること
| 確かめること | はいの場合 |
|---|---|
| キーにする項目は必ず一意か | 2引数でよい。一意でないならマージ関数か groupingBy |
| 値が null になる可能性はあるか | 既定値に置き換える・除外する・put で入れる |
| キーが null になる可能性はあるか | groupingBy を使うなら先に置き換える |
| 結果の順序に意味があるか | 4引数で LinkedHashMap か TreeMap |
| 結果を後から変更するか | 変更しないなら toUnmodifiableMap も選べる |
| Java 8 のログを読んでいるか | Duplicate key の後ろはキーではなく値 |
よくある質問
Q. toMap と groupingBy はどう使い分けますか?
A. 1つのキーに対して値が1つなら toMap、複数の要素をまとめたいなら groupingBy です。合計のように「まとめて1つの値にする」場合は、toMap のマージ関数(Integer::sum)でも、groupingBy と summingInt の組み合わせでも書けます。今回の例ではどちらも 開発=172 になりました。
Q. マージ関数の (a, b) はどちらが先に入っていた値ですか?
A. a がすでにMapに入っている値、b が後から来た値です。今回の実行結果でも、(a, b) -> a はリストで先に出てくる「高橋」、(a, b) -> b は後に出てくる「佐藤」になりました。
Q. 重複しないとわかっているのに、4引数版でマージ関数を書く必要がありますか?
A. 4引数版は3つ目の引数を省略できないため、何かしら書く必要があります。重複しない前提なら、(a, b) -> a と書くほか、重複したら例外を投げる関数((a, b) -> { throw new IllegalStateException("重複: " + a); })にしておくと、2引数版と同じように想定外の重複に気づけます。
Q. recordを使わない通常のクラスでも同じように書けますか?
A. 書けます。Member::id の部分を Member::getId のようにgetterのメソッド参照へ置き換えるだけです。recordは Java 16 以降の機能なので、それより前のバージョンではgetterを持つクラスを使います。
まとめ
- ListからMapへの変換は Collectors.toMap(キー, 値)が基本。値に要素そのものを入れるときは Function.identity() を使う。
- キーが重複すると2引数版は IllegalStateException: Duplicate key で止まる。3つ目の引数で先勝ち
(a, b) -> a、後勝ち(a, b) -> b、合算Integer::sumを選ぶ。 - すべて残したいときは groupingBy でリスト化する。条件で2つに分けるなら partitioningBy。
- 値が null だと引数の数に関係なく NullPointerException になる。既定値への置き換え・除外・put のいずれかで直す。
- 順序を保つには4引数版で LinkedHashMap::new を渡す。キーの昇順なら TreeMap::new。
- Java 8 のエラー文は Duplicate key の後ろが値、Java 11・21 ではキーと2つの値が表示される。
- groupingBy はキーが null だと止まるため、空の可能性がある項目は先に置き換える。
参考・出典
- Java SE 21 API ドキュメント Collectors(toMap、toUnmodifiableMap、groupingBy、partitioningBy): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Collectors.html
- OpenJDK 課題票 JDK-8040892(Collectors.toMap の例外メッセージ): https://bugs.openjdk.org/browse/JDK-8040892
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java ListをMapに変換する方法 toMapのキー重複エラー対処」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。