FlatMap を使ったネスト解除 — ネスト構造展開
「配列の中に配列」「リストの中にリスト」「Optional の中に Optional」など、入れ子になった構造を一段に平らにするのが flatMap。map は「変換のみ」、flatMap は「変換してから平らに伸ばす」です。初心者がつまずきやすいポイントを、例とテンプレートで整理します。
基本の考え方(map と flatMap の違い)
- 直感:
- map: 1要素 → 1値(または 1要素 → 1要素)
- flatMap: 1要素 → 複数要素(Stream)を返し、それらを「つなげて 1本の Stream」にする
- 型の違い:
- map: Stream<T> → Stream<R>
- flatMap: Stream<T> → Stream<R>(ただし「関数は T → Stream<R> を返す」)
- 使いどころ: 「T を処理した結果が複数要素になる」場合(分割、展開、子要素列挙)
すぐ使える基本例
1. List<List<T>> を 1 本の List<T> にする(典型)
import java.util.*;
import java.util.stream.*;
List<List<String>> nested = List.of(
List.of("A", "B"),
List.of("C"),
List.of("D", "E", "F")
);
List<String> flat = nested.stream()
.flatMap(List::stream) // 各 List を Stream に変換して連結
.collect(Collectors.toList());
System.out.println(flat); // [A, B, C, D, E, F]
Java2. 文字列を「単語」に分割して全部つなぐ
List<String> sentences = List.of("hello world", "java stream api");
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split("\\s+")))
.collect(Collectors.toList());
System.out.println(words); // [hello, world, java, stream, api]
Java3. 子要素の列挙(親→子リスト)
record User(String name, List<String> skills) {}
List<User> users = List.of(
new User("Tanaka", List.of("Java", "SQL")),
new User("Sato", List.of("Python")),
new User("Ito", List.of("AWS", "Docker"))
);
List<String> allSkills = users.stream()
.flatMap(u -> u.skills().stream())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(allSkills); // [AWS, Docker, Java, Python, SQL]
Javaよくあるパターン別レシピ
1. Map<K, List<V>> を「ペアのストリーム」に展開
Map<String, List<Integer>> data = Map.of(
"A", List.of(1, 2),
"B", List.of(3)
);
record Pair(String key, Integer val) {}
List<Pair> pairs = data.entrySet().stream()
.flatMap(e -> e.getValue().stream().map(v -> new Pair(e.getKey(), v)))
.collect(Collectors.toList());
// 結果: [Pair(A,1), Pair(A,2), Pair(B,3)]
Java- ねらい: MultiMap の中身を「キーと値の組」へ展開すると集計・フィルタがしやすい。
2. ファイルの複数行を単語に分解(行→単語)
import java.nio.file.*;
import java.io.IOException;
List<String> words = Files.lines(Path.of("sample.txt")) // Stream<String>(各行)
.flatMap(line -> Arrays.stream(line.split("\\W+"))) // 行を単語に
.filter(w -> !w.isBlank())
.collect(Collectors.toList());
Java3. Optional のネスト解除(Optional<Optional<T>> → Optional<T>)
import java.util.Optional;
Optional<String> outer = Optional.of("hello");
Optional<Optional<Integer>> nested = outer.map(s -> Optional.of(s.length())); // Optional<Optional<Integer>>
Optional<Integer> flat = nested.flatMap(x -> x); // ネスト解除
System.out.println(flat.orElse(0)); // 5
Java- ねらい: map は「Optional を包む」、flatMap は「包みを開いて 1 段にする」。
4. 0 個にもなり得る展開(条件に合う時だけ要素化)
List<Integer> nums = List.of(1, 2, 3, 4, 5);
List<Integer> squaresOfEven = nums.stream()
.flatMap(n -> (n % 2 == 0) ? Stream.of(n * n) : Stream.empty())
.collect(Collectors.toList());
System.out.println(squaresOfEven); // [4, 16]
Java- ねらい: 条件で「0 要素」「1 要素」「複数要素」を返せるのが flatMap の強み。
例題で身につける
例題1: CSV の「複数列」を 1 本の値列へ
List<String> csv = List.of(
"A,1,2",
"B,3",
"C,4,5,6"
);
List<String> tokens = csv.stream()
.flatMap(line -> Arrays.stream(line.split(",")))
.collect(Collectors.toList());
System.out.println(tokens); // [A,1,2,B,3,C,4,5,6]
Java例題2: ネストした JSON 風モデルのタグ一覧
record Post(String title, List<String> tags) {}
List<Post> posts = List.of(
new Post("P1", List.of("java", "stream")),
new Post("P2", List.of("java", "collection")),
new Post("P3", List.of("concurrency"))
);
List<String> uniqueTags = posts.stream()
.flatMap(p -> p.tags().stream())
.distinct()
.sorted()
.toList();
System.out.println(uniqueTags); // [collection, concurrency, java, stream]
Java例題3: ネスト解除後に toMap で集計
record Tx(String user, List<Integer> amounts) {}
List<Tx> txs = List.of(
new Tx("Tanaka", List.of(100, 200)),
new Tx("Sato", List.of(300)),
new Tx("Tanaka", List.of(50))
);
// ユーザー別合計
Map<String, Integer> sumByUser = txs.stream()
.flatMap(tx -> tx.amounts().stream().map(a -> Map.entry(tx.user(), a)))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
Integer::sum
));
System.out.println(sumByUser); // {Tanaka=350, Sato=300}
Javaテンプレート集(そのまま使える形)
- List<List<T>> を平らに
List<T> flat = listOfLists.stream()
.flatMap(List::stream)
.toList();
Java- 文字列リスト → 単語リスト
List<String> words = lines.stream()
.flatMap(s -> Arrays.stream(s.split("\\s+")))
.filter(w -> !w.isBlank())
.toList();
Java- Map<K, List<V>> → ペアへ展開
List<Map.Entry<K,V>> pairs = map.entrySet().stream()
.flatMap(e -> e.getValue().stream().map(v -> Map.entry(e.getKey(), v)))
.toList();
Java- 条件付きで 0/1 要素を返す
Stream<R> s = Stream.of(x).flatMap(t -> meets(t) ? Stream.of(transform(t)) : Stream.empty());
Java- Optional の二重包みを解除
Optional<T> o = opt.flatMap(x -> x); // Optional<Optional<T>> → Optional<T>
Java実務でのコツと落とし穴
- 落とし穴: map のままだと「ネストが残る」
- 回避: 「関数の戻りが Stream(や Optional)」になるときは flatMap。map は 1:1 の変換に使う。
- 落とし穴: null を返してしまう
- 回避: flatMap のラムダは null ではなく「空の Stream(Stream.empty())」や「Optional.empty()」を返す。
- 落とし穴: 重い処理をラムダ内でやりすぎ
- 回避: 変換・展開だけに集中させ、I/O は上流/下流に分離。複雑になったら関数を変数に切り出す。
- 落とし穴: 順序や重複の扱い
- 回避: 展開後に必要なら
distinct()、sorted()を明示。集合的扱いはtoSet()/toCollection(LinkedHashSet::new)。
- 回避: 展開後に必要なら
まとめ
- flatMap は「複数になる変換結果を連結して 1 本の流れにする」ための道具。入れ子の構造(List の中の List、行→単語、親→子要素、Optional の二重包み)を直感的に解く。
- 使い分けの鍵は「戻りが 1 要素か、複数要素(Stream/Optional)か」。flatMap でネストを解消してから、distinct・sorted・toMap などで最終形へまとめると、読みやすく安全なパイプラインになる。
