Java 逆引き集 | FlatMap を使ったネスト解除 — ネスト構造展開

Java Java
スポンサーリンク

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]
Java

2. 文字列を「単語」に分割して全部つなぐ

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]
Java

3. 子要素の列挙(親→子リスト)

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());
Java

3. 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 などで最終形へまとめると、読みやすく安全なパイプラインになる。
タイトルとURLをコピーしました