StreamAPIって便利なんですが「さて、どう書くんだっけ?」ってなることありますよね。
特にListとMapを行き来する処理はパターンを覚えてしまえばあとは応用するだけなのですが、久しぶりに書くと手が止まりがちです。このメモを3歩歩いても忘れる自分のために残しておきます。
この記事でわかること
Collectors.toMap()を使ってListからMapへ変換する方法keySet()や Stream を使ってMapのキーをListへ変換する方法filter/map/flatMapなどよく使うStream処理のパターンCollectorsの早見表
ListからMapへ変換する
基本の書き方
Collectors.toMap()を使います。第1引数にキー、第2引数に値を指定するだけです。
Map<K, V> map = list
.stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
具体例
Userレコードのリストをidをキー、nameを値にしたMapへ変換するパターンです。
class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return this.id;
}
public String getName() {
return this.name;
}
}
List<User> users = Arrays.asList(
new User(1, "Alice"),
new User(2, "Bob"),
new User(3, "Charlie")
);
Map<Integer, String> userMap = users
.stream()
.collect(Collectors.toMap(User::getId, User::getName));
// 結果: {1=Alice, 2=Bob, 3=Charlie}
実際のプロジェクトでは、上記のようなUserクラスにLombokの@Getterや@AllArgsConstructorを使ってボイラープレートを省略することが多いと思います。Eclipseの最新版でLombokのインポートエラーが発生している場合はこちらも参考にしてみてください。
重複キーがある場合
リストに同じキーになる要素が2件以上あるとIllegalStateExceptionが発生します。3番目の引数(マージ関数)を渡すことで対処できます。
Map<Integer, String> userMap = users
.stream()
.collect(Collectors.toMap(
User::getId,
User::getName,
(existing, replacement) -> existing // 既存の値を優先する場合
));
注意: valueに
nullが入るとNullPointerExceptionになります。nullを含む可能性があるリストはあらかじめfilterでnullを弾いておくか、HashMapを使う方法(後述)に切り替えるのが無難です。
MapのキーをListへ変換する
keySet()をそのまま使う
Map<Integer, String> map = Map.of(1, "Alice", 2, "Bob", 3, "Charlie");
List<Integer> keyList = new ArrayList<>(map.keySet());
ちなみにMap.of()はJva9から使用可能です。Java8では使えないので、あんまりスマートではないですがStreamを使って下記のような書き方もできます。
Map<Integer, String> map = Stream.of(
new AbstractMap.SimpleEntry<>(1, "Alice"),
new AbstractMap.SimpleEntry<>(2, "Bob"),
new AbstractMap.SimpleEntry<>(3, "Charlie")
)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Streamを使う
Streamを挟むことでソートや絞り込みと組み合わせやすくなります。
// Listに変換するだけ
List<Integer> keyList = map
.keySet()
.stream()
.collect(Collectors.toList());
// ソートして取り出す
List<Integer> sortedKeys = map
.keySet()
.stream()
.sorted()
.collect(Collectors.toList());
補足:
Map.keySet()が返すSetは順序を保証しません。順序が必要な場合はsorted()を挟むかLinkedHashMapを使うと判断できるわけですね。
よく使うStream処理のパターン
filter — 条件を絞り込む
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evens = numbers
.stream()
.filter(n -> (n % 2) == 0)
.collect(Collectors.toList());
// 結果: [2, 4, 6]
map — 要素を変換する
StreamのmapはListのmapではなく「各要素を別の値に変換する」操作です。同名なので少し紛らわしいですが、役割はまったく別物と考えるとわかりやすいです。
List<String> names = List.of("alice", "bob", "charlie");
List<String> upper = names
.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// 結果: [ALICE, BOB, CHARLIE]
数値に変換して合計を出したい場合はmapToInt()が便利です。
int totalId = users
.stream()
.mapToInt(User::id)
.sum();
flatMap — ネストしたListを平坦化する
リストのリストを1つのリストにまとめたいときに使います。
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5, 6)
);
List<Integer> flat = nested
.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
// 結果: [1, 2, 3, 4, 5, 6]
distinct / sorted — 重複除去・ソート
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5);
List<Integer> result = nums
.stream()
.distinct() // 重複を除く
.sorted() // 昇順にソート
.collect(Collectors.toList());
// 結果: [1, 2, 3, 4, 5, 6, 9]
降順にしたい場合はsorted(Comparator.reverseOrder())を使います。
groupingBy — グループ化してMapに変換する
toMap()は1対1のマッピングですが、同じキーに複数の要素を紐づけたい(1対多)場合はgroupingByが向いています。
// 部署IDでグループ化する例(departmentId フィールドがある想定)
Map<Integer, List<User>> grouped = users
.stream()
.collect(Collectors.groupingBy(User::getId));
// グループごとの件数が欲しい場合
Map<Integer, Long> countByDept = users
.stream()
.collect(Collectors.groupingBy(User::getId, Collectors.counting()));
実務ではDBからResultSetやO/Rマッパーで取得したListをStreamでグループ化・変換するパターンが頻出します。PostgreSQLをJDBCで接続している場合、スキーマの指定まわりで手間がかかることがありますが、currentSchemaパラメータを使うと設定ファイル1行で解決できます。
joining — 文字列を結合する
List<String> words = List.of("Java", "Stream", "API");
// 区切り文字だけ
String joined = words
.stream()
.collect(Collectors.joining(", "));
// 結果: "Java, Stream, API"
// 前置・後置を付ける場合
String wrapped = words
.stream()
.collect(Collectors.joining(", ", "[", "]"));
// 結果: "[Java, Stream, API]"
count / findFirst / anyMatch
// 条件に合う件数
long count = users
.stream()
.filter(u -> u.name().startsWith("A"))
.count();
// 条件に合う最初の1件(Optionalで返ってくる)
Optional<User> first = users
.stream()
.filter(u -> u.id() > 1)
.findFirst();
// 条件を満たす要素が1件でもあるか
boolean hasAlice = users
.stream()
.anyMatch(u -> u.name().equals("Alice"));
// 全件が条件を満たすか
boolean allPositive = numbers
.stream()
.allMatch(n -> n > 0);
// 条件を満たす要素が1件もないか
boolean noneNegative = numbers
.stream()
.noneMatch(n -> n < 0);
Streamの中間操作は処理が数珠つなぎになるぶん、「どこで値が変わったのか」が追いにくいことがあります。Eclipseのブレークポイントを条件付きで設定しておくと、特定の要素だけで処理を止めて確認できるので便利です。
Collectors早見表
| Collector | 主な用途 |
|---|---|
toList() | Listに変換する。 |
toUnmodifiableList() | 変更不可のListに変換する。 |
toSet() | Setに変換する(重複が自動で除去される)。 |
toMap(k, v) | Mapに変換する(1対1)。 |
groupingBy(f) | グループ化してMap<K, List<V>>に変換する(1対多)。 |
joining(区切り) | 文字列を結合する。 |
counting() | groupingBy との組み合わせでグループ内件数を集計する。 |
toUnmodifiableMap(k, v) | 変更不可のMapに変換する。 |
Q&A
- Q
Stream.toList()とCollectors.toList()の違いは? - A
Java 16以降は
stream.toList()と書けるようになりました。ただしこちらは変更不可(Unmodifiable)なListを返します。add()やremove()を後から呼ぶとUnsupportedOperationExceptionになるので注意が必要です。変更が必要なListが欲しい場合はCollectors.toList()かnew ArrayList<>(stream.toList())で対処できます。
- Q
toMap()でnullがあるとエラーになる? - A
Collectors.toMap()はvalueがnullだとNullPointerExceptionが発生します。回避策の一つはマージ関数に加えてマップファクトリを渡しHashMapを明示する方法です。JavaMap<Integer, String> map = users .stream() .collect(Collectors.toMap( User::id, u -> u.name(), // null になりうるvalue (a, b) -> a, HashMap::new // nullを許容するHashMapを使う ));
- QStreamは再利用できる?
- A
一度
collect()やforEach()など終端操作を呼び出すとそのStreamはもう使えなくなります。再度処理したい場合はlist.stream()から作り直す必要があります。
まとめ
- ListからMapへの変換は
Collectors.toMap(keyMapper, valueMapper)が基本だよ。 - 重複キーがあるときは第3引数のマージ関数を忘れずに渡してね。
- MapのキーをListにするだけなら
new ArrayList<>(map.keySet())が一番シンプルだよ。 - Streamは中間操作(
filter/map/sortedなど)を繋げて、終端操作(collect/count/findFirstなど)で結果を出す流れだよ。 groupingByは1対多のグループ化、toMapは1対1のマッピングと使い分けてね。



