- JAVA
Java Stream flatMapの使い方 mapとの違いとmapMulti
flatMap とは、ストリームの各要素を「0個以上の要素が流れるストリーム」に変換し、それらを1本のストリームにつなぎ直す中間操作です。 map が「1個を1個に変える」のに対して、flatMap は「1個を何個かに広げて、入れ子をほどく」役割を持ちます。リストのリストを1つのリストにまとめる、親オブジェクトの一覧から子の一覧を取り出す、といった処理が1行で書けます。
この記事では、同じ入力に map と flatMap をかけた結果を並べて違いを確認したあと、よく使う5つの定番コードを実行結果つきで紹介します。子リストが null のときに起きる NullPointerException と直し方、Optional.flatMap、flatMapToInt、Java 16 で追加された mapMulti との書き比べまで扱います。
確認環境:Ubuntu 24.04/OpenJDK 21.0.10(java ファイル名.java の単一ファイル実行)。各メソッドが追加されたバージョンは、OpenJDK 21 のソースに含まれる Javadoc コメント(@since)を開いて確認しました。
flatMapと関連メソッドの早見表
| メソッド | 何をするか | 結果の型(要素が String の例) | 追加 |
|---|---|---|---|
Stream.map |
1要素を1要素に変換する | Stream<R> |
8 |
Stream.flatMap |
1要素を0個以上に展開し、1本につなぐ | Stream<R> |
8 |
Stream.flatMapToInt |
展開した結果を IntStream にする(ToLong、ToDouble もある) | IntStream |
8 |
Stream.mapMulti |
要素ごとに sink.accept を呼んだ回数だけ流す | Stream<R> |
16 |
Stream.mapMultiToInt |
mapMulti の IntStream 版 | IntStream |
16 |
Stream.ofNullable |
null なら空、値があれば1要素のストリームを作る | Stream<T> |
9 |
Optional.flatMap |
Optional を返す処理をつないでも二重にしない | Optional<U> |
8 |
Optional.stream |
値があれば1要素、無ければ空のストリームにする | Stream<T> |
9 |
Collectors.flatMapping |
groupingBy などの集計の中で平坦化する | Collector | 9 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java Stream flatMapの使い方 mapとの違いとmapMulti」
| やりたいこと | 書き方 |
|---|---|
| リストのリストを1つにする | lists.stream().flatMap(List::stream).toList() |
| 2次元配列を1次元にする | Arrays.stream(table).flatMap(Arrays::stream) |
| 2次元の int 配列を1次元にする | Arrays.stream(grid).flatMapToInt(Arrays::stream).toArray() |
| 文を単語に分ける | lines.stream().flatMap(s -> Arrays.stream(s.split(" "))) |
| 親の一覧から子の一覧を取る | orders.stream().flatMap(o -> o.items().stream()) |
| 全組み合わせを作る | a.stream().flatMap(x -> b.stream().map(y -> x + y)) |
| null の子リストを飛ばす | flatMap(o -> Stream.ofNullable(o.items())).flatMap(List::stream) |
| 中身のある Optional だけ残す | flatMap(Optional::stream) |
map と flatMap の違いを同じ入力で比べる
3つのチームのメンバー一覧(リストのリスト)に、map と flatMap をそれぞれかけてみます。変数の型に注目してください。
import java.util.List;
import java.util.stream.Stream;
public class MapVsFlatMap {
public static void main(String[] args) {
List<List<String>> teams = List.of(
List.of("sato", "suzuki"),
List.of("tanaka"),
List.of("ito", "kato"));
// map: 1要素 → 1要素。リストはリストのまま流れる
Stream<List<String>> mapped = teams.stream().map(list -> list);
List<List<String>> r1 = mapped.toList();
System.out.println("map : " + r1 + " 要素数=" + r1.size());
// map(List::stream): Stream の Stream になる
Stream<Stream<String>> nested = teams.stream().map(List::stream);
System.out.println("map(List::stream) : " + nested.toList());
// flatMap: 1要素 → 0個以上の要素。内側の Stream がつながって1本になる
Stream<String> flat = teams.stream().flatMap(List::stream);
List<String> r2 = flat.toList();
System.out.println("flatMap : " + r2 + " 要素数=" + r2.size());
}
}
実行結果:
map : [[sato, suzuki], [tanaka], [ito, kato]] 要素数=3
map(List::stream) : [java.util.stream.ReferencePipeline$Head@3d680b5a, java.util.stream.ReferencePipeline$Head@4b5d6a01, java.util.stream.ReferencePipeline$Head@4a22f9e2]
flatMap : [sato, suzuki, tanaka, ito, kato] 要素数=5
| 操作 | できあがる型 | 出力 | 要素数 |
|---|---|---|---|
map(list -> list) |
Stream<List<String>> |
[[sato, suzuki], [tanaka], [ito, kato]] |
3 |
map(List::stream) |
Stream<Stream<String>> |
ストリームのオブジェクトが3個 | 3 |
flatMap(List::stream) |
Stream<String> |
[sato, suzuki, tanaka, ito, kato] |
5 |
map は入力が3個なら出力も必ず3個です。 map(List::stream) と書いても、ストリームが3個入ったストリームになるだけで、中身は取り出されません。出力の java.util.stream.ReferencePipeline$Head@… は、ストリームのオブジェクトそのものが表示されたものです。
flatMap は、ラムダ式が返した3本のストリームを順番につないで1本にします。 そのため型は Stream<String> になり、要素数は 2+1+2 の5個になりました。「map して、平らに(flat)する」と読むと覚えやすい名前です。
flowchart LR
A["[sato, suzuki]"] --> F[flatMap]
B["[tanaka]"] --> F
C["[ito, kato]"] --> F
F --> R["sato, suzuki, tanaka, ito, kato"]
使い分けの目安は次のとおりです。
| ラムダ式が返すもの | 選ぶ操作 |
|---|---|
| 1個の値(文字列、数値、オブジェクト) | map |
| リスト・配列・ストリームなど「複数の値」で、ほどいて並べたい | flatMap |
| リストのまま次の処理に渡したい | map |
コピペで使える flatMap の定番5パターン
5つの定番を1つのファイルにまとめました。先頭の番号が実行結果の番号に対応しています。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Patterns {
record Item(String name, int qty) {}
record Order(String id, List<Item> items) {}
public static void main(String[] args) {
// 1. リストのリストを平坦化
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3), List.of(), List.of(4, 5));
List<Integer> flat = nested.stream().flatMap(List::stream).toList();
System.out.println("1: " + flat);
// 2. 2次元配列を平坦化
String[][] table = {{"a", "b"}, {"c"}, {"d", "e"}};
List<String> cells = Arrays.stream(table).flatMap(Arrays::stream).toList();
System.out.println("2: " + cells);
int[][] grid = {{1, 2, 3}, {4, 5, 6}};
int[] line = Arrays.stream(grid).flatMapToInt(Arrays::stream).toArray();
System.out.println("2: " + Arrays.toString(line));
// 3. 文字列を分割して単語の一覧にする
List<String> lines = List.of("java stream flatMap", "map と flatMap", "java");
List<String> words = lines.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.distinct()
.toList();
System.out.println("3: " + words);
// 4. 親子オブジェクトから子の一覧を取り出す
List<Order> orders = List.of(
new Order("A-1", List.of(new Item("pen", 2), new Item("note", 1))),
new Order("A-2", List.of()),
new Order("A-3", List.of(new Item("pen", 5))));
List<Item> items = orders.stream().flatMap(o -> o.items().stream()).toList();
System.out.println("4: " + items);
// 親の情報も残したいときは flatMap の中で map する
List<String> rows = orders.stream()
.flatMap(o -> o.items().stream().map(i -> o.id() + ":" + i.name() + "x" + i.qty()))
.toList();
System.out.println("4: " + rows);
System.out.println("4: " + orders.stream()
.flatMap(o -> o.items().stream())
.collect(Collectors.groupingBy(Item::name, Collectors.summingInt(Item::qty))));
// 5. 2つのリストの全組み合わせ
List<String> sizes = List.of("S", "M", "L");
List<String> colors = List.of("red", "blue");
List<String> pairs = sizes.stream()
.flatMap(s -> colors.stream().map(c -> s + "-" + c))
.toList();
System.out.println("5: " + pairs + " 件数=" + pairs.size());
}
}
実行結果:
1: [1, 2, 3, 4, 5]
2: [a, b, c, d, e]
2: [1, 2, 3, 4, 5, 6]
3: [java, stream, flatMap, map, と]
4: [Item[name=pen, qty=2], Item[name=note, qty=1], Item[name=pen, qty=5]]
4: [A-1:penx2, A-1:notex1, A-3:penx5]
4: {note=1, pen=7}
5: [S-red, S-blue, M-red, M-blue, L-red, L-blue] 件数=6
1. リストのリストを平坦化する
flatMap(List::stream) が基本形です。空のリスト List.of() は要素0個のストリームになるので、結果には何も残りません。 特別な分岐を書かなくても、空の子は自然に飛ばされます。
2. 2次元配列を平坦化する
配列は Arrays.stream でストリームにします。String[][] は flatMap(Arrays::stream)、int[][] は flatMapToInt(Arrays::stream) で IntStream にすると、toArray で int[] に戻せます。
3. 文字列を分割して単語の一覧にする
split(" ") は配列を返すので、Arrays.stream で包んで flatMap に渡します。ここでは distinct を続けて重複を除きました。3行目の入力 "java" は1行目で出ているので、結果に2回は出てきません。
4. 親子オブジェクトから子の一覧を取り出す
注文(Order)が明細(Item)のリストを持つ形です。flatMap(o -> o.items().stream()) で全注文の明細が1本になります。明細が空の A-2 は結果に現れません。
親の情報も一緒に使いたいときは、flatMap のラムダ式の中で map します。 o.items().stream().map(i -> o.id() + ":" + ...) のように書くと、ラムダ式の中では親の o と子の i の両方を参照できます。3つ目の例のように、平坦化してから groupingBy で集計する流れもよく使います。
5. 2つのリストの全組み合わせを作る
外側のリストを flatMap し、内側のリストを map します。サイズ3種×色2種で6件になりました。for 文の二重ループと同じ順番(外側が先)で並びます。
子リストが null のときの NullPointerException と直し方
データの取得元によっては、子リストが空リストではなく null で届くことがあります。その場合の動きを再現します。
import java.util.List;
public class NullChild {
record Order(String id, List<String> items) {}
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("A-1", List.of("pen", "note")),
new Order("A-2", null), // 子リストが null
new Order("A-3", List.of("ink")));
List<String> items = orders.stream()
.flatMap(o -> o.items().stream()) // A-2 で null.stream() になる
.toList();
System.out.println(items);
}
}
実行結果(先頭3行):
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.stream()" because the return value of "NullChild$Order.items()" is null
at NullChild.lambda$main$0(NullChild.java:13)
at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:273)
(以下略)
メッセージは「NullChild$Order.items() の戻り値が null なので List.stream() を呼べない」という意味です。例外は flatMap そのものではなく、ラムダ式の中の o.items().stream() で起きています。 直し方を3つ並べます。
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
public class NullFix {
record Order(String id, List<String> items) {}
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("A-1", List.of("pen", "note")),
new Order("A-2", null),
new Order("A-3", List.of("ink")));
// 直し方1: Stream.ofNullable (Java 9) で null を空の Stream に変える
List<String> a = orders.stream()
.flatMap(o -> Stream.ofNullable(o.items())) // null なら 0 個、あれば List が 1 個
.flatMap(List::stream)
.toList();
System.out.println("ofNullable : " + a);
// 直し方2: 先に filter で除く
List<String> b = orders.stream()
.map(Order::items)
.filter(Objects::nonNull)
.flatMap(List::stream)
.toList();
System.out.println("filter nonNull : " + b);
// 直し方3: 三項演算子で空の Stream を返す
List<String> c = orders.stream()
.flatMap(o -> o.items() == null ? Stream.empty() : o.items().stream())
.toList();
System.out.println("Stream.empty : " + c);
// 参考: ラムダが null を返した場合は空の Stream として扱われる
List<String> d = orders.stream()
.flatMap(o -> o.items() == null ? null : o.items().stream())
.toList();
System.out.println("null を返す : " + d);
// 子リストの中の null 要素は flatMap では消えない
List<List<String>> withNull = Arrays.asList(Arrays.asList("x", null), Arrays.asList("y"));
System.out.println("要素が null : " + withNull.stream().flatMap(Collection::stream).toList());
System.out.println("nonNull で除く : " + withNull.stream().flatMap(Collection::stream)
.filter(Objects::nonNull).toList());
}
}
実行結果:
ofNullable : [pen, note, ink]
filter nonNull : [pen, note, ink]
Stream.empty : [pen, note, ink]
null を返す : [pen, note, ink]
要素が null : [x, null, y]
nonNull で除く : [x, y]
| 直し方 | 書き方 | 特徴 |
|---|---|---|
| Stream.ofNullable(Java 9) | flatMap(o -> Stream.ofNullable(o.items())).flatMap(List::stream) |
null なら0個、あればリスト1個のストリームになる。判定を式で書かずに済む |
| 先に filter | map(Order::items).filter(Objects::nonNull).flatMap(List::stream) |
Java 8 でも使える。意図が読み取りやすい |
| 三項演算子 | o.items() == null ? Stream.empty() : o.items().stream() |
1つのラムダ式で完結する |
Stream.ofNullable は「null かもしれない1個の値」をストリームにするメソッドです。 リストそのものを1要素として流すので、もう一度 flatMap(List::stream) でほどく2段構えになります。
4つ目の例のとおり、ラムダ式が null を返した場合は空のストリームとして扱われ、例外にはなりませんでした。Stream の Javadoc にも「変換結果のストリームが null の場合は、代わりに空のストリームが使われる」と書かれています。ただし、読む人に意図が伝わりやすいのは Stream.empty() を返す書き方です。
また、子リストの中に入っている null の要素は、flatMap では取り除かれません。 結果は [x, null, y] でした。除きたいときは filter(Objects::nonNull) を続けます。
Optional.flatMap:Optional の二重化を避ける
Optional にも flatMap があります。考え方は Stream と同じで、「Optional を返す処理」を map でつなぐと Optional<Optional<T>> になり、flatMap でつなぐと Optional<T> のままになります。
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class OptionalFlat {
record User(String name, String managerId) {}
static final Map<String, User> USERS = new HashMap<>(Map.of(
"u1", new User("sato", "u2"),
"u2", new User("suzuki", null)));
static Optional<User> findUser(String id) {
return Optional.ofNullable(USERS.get(id));
}
public static void main(String[] args) {
// map だと Optional が二重になる
Optional<Optional<User>> doubled = findUser("u1").map(u -> findUser(u.managerId()));
System.out.println("map : " + doubled);
// flatMap なら1重のまま
Optional<User> manager = findUser("u1").flatMap(u -> findUser(u.managerId()));
System.out.println("flatMap : " + manager);
// 途中で空になれば、最後まで空のまま進む
System.out.println("上司なし: " + findUser("u2").flatMap(u -> findUser(u.managerId())).map(User::name));
System.out.println("本人なし: " + findUser("u9").flatMap(u -> findUser(u.managerId())).map(User::name));
System.out.println("名前 : " + findUser("u1").flatMap(u -> findUser(u.managerId())).map(User::name).orElse("(なし)"));
// Stream の中の Optional を、中身のあるものだけに絞る (Java 9 の Optional.stream)
List<String> names = List.of("u1", "u9", "u2").stream()
.map(OptionalFlat::findUser)
.flatMap(Optional::stream)
.map(User::name)
.toList();
System.out.println("Optional::stream : " + names);
}
}
実行結果:
map : Optional[Optional[User[name=suzuki, managerId=null]]]
flatMap : Optional[User[name=suzuki, managerId=null]]
上司なし: Optional.empty
本人なし: Optional.empty
名前 : suzuki
Optional::stream : [sato, suzuki]
- 1行目:map では
Optional[Optional[User[...]]]と二重になり、中身を使うのに2回取り出す必要があります。 - 2行目:flatMap なら1重です。続けて
map(User::name)やorElseを書けます。 - 3〜4行目:本人が見つからない場合も、上司がいない場合も、途中で空になればそのまま
Optional.emptyが最後まで流れます。if 文での null 判定を重ねずに書けるのが利点です。 - 6行目:ストリームの中に Optional が並んでいるときは
flatMap(Optional::stream)で、中身のあるものだけを取り出せます。 見つからなかった u9 は結果から消えています。
| 呼び出す処理の戻り値 | Optional でのつなぎ方 |
|---|---|
| ふつうの値(String など) | map |
| Optional | flatMap |
flatMapToInt:数値にして合計や平均を求める
展開した結果が数値なら、flatMapToInt で IntStream にすると sum、average、max がそのまま使えます。
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
public class FlatMapToInt {
public static void main(String[] args) {
List<int[]> scores = List.of(new int[]{70, 80}, new int[]{90}, new int[]{60, 100});
// int[] のリスト → IntStream。そのまま sum や average が使える
IntStream all = scores.stream().flatMapToInt(Arrays::stream);
System.out.println("sum : " + all.sum());
System.out.println("average : " + scores.stream().flatMapToInt(Arrays::stream).average().getAsDouble());
System.out.println("max : " + scores.stream().flatMapToInt(Arrays::stream).max().getAsInt());
// List<List<Integer>> から IntStream にする
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4, 5));
System.out.println("sum : " + nested.stream()
.flatMapToInt(list -> list.stream().mapToInt(Integer::intValue)).sum());
// 文字列を1文字ずつに分ける (chars は IntStream を返す)
List<String> words = List.of("ab", "cd");
System.out.println("chars : " + words.stream().flatMapToInt(String::chars)
.mapToObj(c -> String.valueOf((char) c)).toList());
// IntStream 自身の flatMap: 1..3 のそれぞれを 1..n に展開
System.out.println("IntStream.flatMap : " + Arrays.toString(
IntStream.rangeClosed(1, 3).flatMap(n -> IntStream.rangeClosed(1, n)).toArray()));
// flatMap だと Stream<Integer> になり、sum は使えないので mapToInt が要る
System.out.println("flatMap+mapToInt : " + nested.stream()
.flatMap(List::stream).mapToInt(Integer::intValue).sum());
}
}
実行結果:
sum : 400
average : 80.0
max : 100
sum : 15
chars : [a, b, c, d]
IntStream.flatMap : [1, 1, 2, 1, 2, 3]
flatMap+mapToInt : 15
List<int[]>はflatMapToInt(Arrays::stream)で1本の IntStream になります。List<List<Integer>>は、ラムダ式の中でmapToInt(Integer::intValue)して IntStream を返します。String::charsは IntStream を返すので、文字単位に分けるときも flatMapToInt が使えます。- IntStream 自身にも flatMap があり、
1, 1, 2, 1, 2, 3のように数値を数値の並びへ展開できます。 - 最後の例のように
flatMap(List::stream).mapToInt(...)と2段で書いても結果は同じ 15 です。
Java 16 の mapMulti と書き比べる
mapMulti は Java 16 で追加された中間操作です。flatMap が「ストリームを返す」のに対し、mapMulti は「要素と sink(受け口)を受け取り、流したい値を sink.accept に渡す」 形で書きます。accept を呼んだ回数だけ下流に要素が流れます。
import java.util.List;
public class MapMulti {
record Item(String name, int qty) {}
record Order(String id, List<Item> items) {}
public static void main(String[] args) {
List<List<String>> teams = List.of(List.of("sato", "suzuki"), List.of("tanaka"), List.of());
// flatMap 版
List<String> a = teams.stream().flatMap(List::stream).toList();
// mapMulti 版 (Java 16): sink.accept を呼んだ回数だけ要素が流れる
List<String> b = teams.stream()
.<String>mapMulti((list, sink) -> {
for (String s : list) {
sink.accept(s);
}
})
.toList();
// Iterable.forEach にメソッド参照で渡す書き方
List<String> c = teams.stream().<String>mapMulti(Iterable::forEach).toList();
System.out.println("flatMap : " + a);
System.out.println("mapMulti : " + b);
System.out.println("forEach : " + c);
// 絞り込みと変換を1回で書く
List<Object> mixed = List.of(1, "two", 3, 4.0, 5);
List<Integer> ints = mixed.stream()
.<Integer>mapMulti((o, sink) -> {
if (o instanceof Integer i) {
sink.accept(i * 10);
}
})
.toList();
System.out.println("絞り込み+変換 : " + ints);
// 親子の展開
List<Order> orders = List.of(
new Order("A-1", List.of(new Item("pen", 2), new Item("note", 1))),
new Order("A-3", List.of(new Item("pen", 5))));
List<String> rows = orders.stream()
.<String>mapMulti((o, sink) -> o.items().forEach(i -> sink.accept(o.id() + ":" + i.name())))
.toList();
System.out.println("親子 : " + rows);
// 数値用の mapMultiToInt
int total = orders.stream()
.mapMultiToInt((o, sink) -> o.items().forEach(i -> sink.accept(i.qty())))
.sum();
System.out.println("mapMultiToInt sum : " + total);
}
}
実行結果:
flatMap : [sato, suzuki, tanaka]
mapMulti : [sato, suzuki, tanaka]
forEach : [sato, suzuki, tanaka]
絞り込み+変換 : [10, 30, 50]
親子 : [A-1:pen, A-1:note, A-3:pen]
mapMultiToInt sum : 8
1〜3行目のとおり、flatMap 版と mapMulti 版は同じ結果になりました。4行目は「Integer だけを選び、10倍にする」という絞り込みと変換を1回の mapMulti で書いた例です。条件に合わないときは accept を呼ばないだけで済みます。
mapMulti で型を指定する理由
上のコードでは .<String>mapMulti(...) と型を明示しています。これを省くと、次のコードはコンパイルエラーになります。
import java.util.List;
public class MapMultiErr {
public static void main(String[] args) {
List<List<String>> teams = List.of(List.of("sato", "suzuki"), List.of("tanaka"));
List<String> names = teams.stream()
.mapMulti((list, sink) -> list.forEach(sink))
.toList();
System.out.println(names);
}
}
MapMultiErr.java:8: error: incompatible types: List<Object> cannot be converted to List<String>
.toList();
^
1 error
ラムダ式だけでは結果の要素型を推論できず Object になるためです。.<String>mapMulti(...) のようにメソッド名の前に型を書くと解決します。
flatMap と mapMulti の書き比べ表
| 観点 | flatMap | mapMulti |
|---|---|---|
| 渡すもの | 要素を受け取りストリームを返す関数 | 要素と sink を受け取り、sink.accept を呼ぶ処理 |
| 0個にしたいとき | Stream.empty() を返す |
accept を呼ばない |
| 結果の型の指定 | 多くの場合は推論される | .<String>mapMulti(...) と書く場面がある |
| 向いている場面 | すでにリストやストリームを持っていて、ほどくだけ | 1要素を少数(0個を含む)に展開する、if や for で書きたい |
| 数値用 | flatMapToInt など | mapMultiToInt など |
| 追加 | Java 8 | Java 16 |
Stream の Javadoc では、mapMulti が flatMap より適する場面として「各要素を少数(0個を含む)の要素に置き換えるとき(結果のグループごとに新しい Stream を作らずに済む)」と「結果をストリームの形で返すより、命令的に生成するほうが書きやすいとき」の2つが挙げられています。子リストをそのままほどくだけなら flatMap、条件つきで出したり出さなかったりするなら mapMulti、と考えると選びやすくなります。
応用:遅延評価・Map の値・集計の中での平坦化
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Lazy {
record Article(String title, List<String> tags) {}
public static void main(String[] args) {
// flatMap の中身は必要な分だけ取り出される
String first = Stream.of("a", "b", "c")
.flatMap(s -> Stream.of(s + "1", s + "2", s + "3")
.peek(x -> System.out.println(" 生成: " + x)))
.findFirst().get();
System.out.println("findFirst = " + first);
// 順序は「外側の順 → 内側の順」
System.out.println(Stream.of(List.of(3, 1), List.of(2)).flatMap(List::stream).toList());
System.out.println(Stream.of(List.of(3, 1), List.of(2)).flatMap(List::stream).sorted().toList());
// Map<String, List<String>> の値をまとめる
Map<String, List<String>> byDept = new TreeMap<>(Map.of(
"dev", List.of("sato", "suzuki"),
"ops", List.of("tanaka")));
System.out.println(byDept.values().stream().flatMap(List::stream).toList());
System.out.println(byDept.entrySet().stream()
.flatMap(e -> e.getValue().stream().map(v -> e.getKey() + "/" + v)).toList());
// 集計の中で平坦化する Collectors.flatMapping (Java 9)
List<Article> articles = List.of(
new Article("A", List.of("java", "stream")),
new Article("B", List.of("java")),
new Article("C", List.of("sql", "java")));
Map<Integer, java.util.Set<String>> tagsByCount = articles.stream()
.collect(Collectors.groupingBy(a -> a.tags().size(), TreeMap::new,
Collectors.flatMapping(a -> a.tags().stream(), Collectors.toCollection(java.util.TreeSet::new))));
System.out.println(tagsByCount);
// タグごとの記事数
Map<String, Long> countByTag = articles.stream().flatMap(a -> a.tags().stream())
.collect(Collectors.groupingBy(t -> t, TreeMap::new, Collectors.counting()));
System.out.println(countByTag);
// 3重のネストは flatMap を2回
List<List<List<Integer>>> cube = List.of(List.of(List.of(1, 2), List.of(3)), List.of(List.of(4)));
System.out.println(cube.stream().flatMap(List::stream).flatMap(List::stream).toList());
// 複数のリストをつなぐ
System.out.println(Stream.of(List.of(1, 2), List.of(3), List.of(4, 5)).flatMap(List::stream).toList());
System.out.println(Stream.concat(List.of(1, 2).stream(), List.of(3).stream()).toList());
}
}
実行結果:
生成: a1
findFirst = a1
[3, 1, 2]
[1, 2, 3]
[sato, suzuki, tanaka]
[dev/sato, dev/suzuki, ops/tanaka]
{1=[java], 2=[java, sql, stream]}
{java=3, sql=1, stream=1}
[1, 2, 3, 4]
[1, 2, 3, 4, 5]
[1, 2, 3]
- 必要な分だけ取り出される:findFirst で最初の1件を求めたとき、「生成」と表示されたのは a1 の1回だけでした。b や c の分は作られていません。
- 順序:外側の順、その中で内側の順に並びます。
[3, 1]と[2]は[3, 1, 2]になり、並べ替えたいときは sorted を続けます。 - Map の値:
values().stream().flatMap(List::stream)で全部の値を1本にできます。キーも使うなら entrySet からたどります。 - Collectors.flatMapping(Java 9):groupingBy の集計の中で平坦化します。ここではタグの数ごとに、使われているタグを重複なしでまとめました。
- 件数の集計:平坦化してから
groupingBy(..., counting())で、タグごとの記事数を数えられます。 - 3重のネスト:flatMap を2回続けます。
- リストの連結:2つなら
Stream.concat、3つ以上ならStream.of(...).flatMap(List::stream)が書きやすい形です。
同じストリームを繰り返し返すと止まる
flatMap のラムダ式は、呼ばれるたびに新しいストリームを返す必要があります。
import java.util.List;
import java.util.stream.Stream;
public class Reuse {
public static void main(String[] args) {
Stream<String> shared = Stream.of("x", "y");
// 同じ Stream のインスタンスを2回返すと2回目で止まる
List<String> r = Stream.of(1, 2).flatMap(n -> shared).toList();
System.out.println(r);
}
}
実行結果(先頭2行):
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.base/java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
(以下略)
ストリームは一度しか使えないため、2回目の要素で IllegalStateException になりました。元になるリストを持っておき、ラムダ式の中で list.stream() を呼んで毎回新しく作る と解決します。
よくある質問
Q. map と flatMap のどちらを使うか迷ったら?
A. ラムダ式が返すものを見ます。1個の値なら map、リストやストリームなど複数の値で、それをほどいて並べたいなら flatMap です。map のまま進めて型が Stream<List<...>> になっていたら、flatMap に替える合図です。
Q. flatMap の中で null を返してもよいですか?
A. この記事の実行結果では、null を返すと空のストリームとして扱われ、例外にはなりませんでした。ただし意図を明確にするなら Stream.empty() を返すか、Stream.ofNullable を使う書き方をおすすめします。
Q. mapMulti は flatMap の代わりにいつも使うべきですか?
A. いいえ。子リストをほどくだけなら flatMap(List::stream) が短く読みやすい書き方です。1要素から0〜数個を条件つきで流すときに mapMulti が向いています。Java 16 以降で使えます。
Q. flatMap したあとの順番は保証されますか? A. リストのように順序のある入力では、実行結果のとおり「外側の順、その中で内側の順」になりました。並べ替えが必要なら sorted、重複を除くなら distinct を続けます。
まとめ
- map は1個を1個に、flatMap は1個を0個以上に展開して1本につなぐ。 型は
Stream<List<String>>とStream<String>の違いになります。 - 定番は
flatMap(List::stream)。 配列はArrays::stream、文字列の分割はArrays.stream(s.split(...))を渡します。 - 親子の展開や全組み合わせは、flatMap のラムダ式の中で map する。 親と子の両方を参照できます。
- 子リストが null なら
Stream.ofNullable、filter、三項演算子のいずれかで空として扱う。 要素の null はfilter(Objects::nonNull)で除きます。 - Optional を返す処理は Optional.flatMap でつなぐ。 ストリーム内の Optional は
flatMap(Optional::stream)で中身だけ残せます。 - 数値は flatMapToInt で IntStream にすると sum や average が使える。
- Java 16 の mapMulti は、条件つきで少数の要素を流す場面に向く。 型は
.<String>mapMulti(...)のように指定します。
参考・出典
- OpenJDK 21 のソース(openjdk-21-source に含まれる java.base/java/util/stream/Stream.java、java/util/Optional.java、java/util/stream/Collectors.java の Javadoc コメント。flatMap・mapMulti の説明と各メソッドの @since)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java Stream flatMapの使い方 mapとの違いとmapMulti」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。