Java 逆引き集 | filter・map・flatMap の使い分け — 条件絞りと展開

Java Java
スポンサーリンク

filter・map・flatMap の使い分け — 条件絞りと展開

「要素を絞る・変換する・入れ子をほどく」をそれぞれ得意とするのが filter、map、flatMap。違いが腹落ちすると、無駄な中間リストを作らずに、短く読みやすいパイプラインが書けます。


基本の考え方(3つの役割)

  • filter: 条件で“残す”
    • ねらい: 要らない要素を早めに捨てる(後段の処理を軽くする)。
    • 返り値: 同じ型のストリーム(要素数は減ることがある)。
  • map: 1→1 の“変換”
    • ねらい: 各要素を別の型や値へ置き換える。
    • 返り値: 型が変わり得る新ストリーム(要素数は変わらない)。
  • flatMap: 1→0..N の“展開+平坦化”
    • ねらい: 「各要素がコレクション(やストリーム)を返す」ケースで、入れ子をほどいて一列にする。
    • 返り値: 展開後の要素の連結ストリーム(要素数は増減する)。

すぐ試せる基本例

filter(絞り込み)

List<Integer> nums = List.of(1,2,3,4,5,6);
List<Integer> evens = nums.stream()
                          .filter(n -> n % 2 == 0)
                          .toList(); // [2,4,6]
Java

map(変換)

List<String> words = List.of("java", "stream");
List<Integer> lengths = words.stream()
                             .map(String::length)
                             .toList(); // [4,6]
Java

flatMap(展開+平坦化)

List<List<String>> names = List.of(
    List.of("Tanaka", "Sato"),
    List.of("Ito")
);

List<String> flat = names.stream()
                         .flatMap(List::stream)
                         .toList(); // ["Tanaka", "Sato", "Ito"]
Java

例題で理解する使い分け

例題1: 文を単語に分解して一列に(flatMap)

List<String> sentences = List.of("hello world", "java stream api");

List<String> tokens = sentences.stream()
    .map(s -> s.split("\\s+"))        // 1文 → 単語配列(map)
    .flatMap(Arrays::stream)          // 配列ストリームへ → 平坦化(flatMap)
    .toList();                        // ["hello", "world", "java", "stream", "api"]
Java
  • ポイント: map で「配列化」→ flatMap で「配列ごと展開」。

例題2: Optional の“中身だけ”を集める(flatMap)

List<Optional<String>> opts = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));

List<String> present = opts.stream()
    .flatMap(o -> o.stream())   // Java 9+ Optional.stream() で中身ありのみ流れる
    .toList();                  // ["A", "B"]
``]
- **ポイント:** null ではなく Optional を使うと、flatMap で欠損を自然に除外できる。

### 例題3: ユーザー→スキル一覧(重複除去も)
```java
record User(String name, List<String> skills) {}

List<User> users = List.of(
    new User("Alice", List.of("Java", "SQL")),
    new User("Bob",   List.of("Python", "SQL"))
);

List<String> uniqueSkills = users.stream()
    .flatMap(u -> u.skills().stream()) // 各ユーザーの技能を展開
    .distinct()                         // 重複排除
    .sorted()
    .toList();                          // ["Java", "Python", "SQL"]
Java
  • ポイント: 「1→複数」な関係は flatMap が自然。

例題4: 先に絞ってから変換(filter→map)

record Product(String name, int price) {}

List<Product> ps = List.of(
    new Product("A", 100), new Product("B", 250), new Product("C", 160)
);

List<String> namesOver200 = ps.stream()
    .filter(p -> p.price() >= 200) // 絞る
    .map(Product::name)            // 変換
    .toList();                     // ["B"]
Java
  • ポイント: 「軽い絞り込み→変換」の順で無駄を減らす。

実用レシピ(そのまま使える形)

  • 配列/コレクションの平坦化
stream.map(this::toArray)
      .flatMap(Arrays::stream)
      .toList();
Java
  • Optional の抽出(欠損除外)
stream.flatMap(Optional::stream).toList();
Java
  • ネスト構造の展開(List<List<T>> → List<T>)
listOfLists.stream().flatMap(List::stream).toList();
Java
  • 条件付きで変換(フィルタ→変換)
stream.filter(this::cond)
      .map(this::transform)
      .toList();
Java
  • プリミティブ展開(flatMapToInt/Long/Double)
stream.flatMapToInt(this::toIntStream).sum();
Java

迷いやすいポイントと回避策

  • map と flatMap の違いを混同:
    • 回避: 「map は1→1」「flatMap は1→0..N+平坦化」。List や Optional を返すなら flatMap を検討。
  • 中間リストを作りすぎる:
    • 回避: map で List を返してから collect しない。flatMap で直接流して最後に collect。
  • null を流す事故:
    • 回避: null を返さない。Optional を使い、flatMap(Optional::stream) か filter(Objects::nonNull) を使う。
  • 順序とコストの最適化を忘れる:
    • 回避: 先に filter(軽い判定)→ 後で flatMap/map。distinct/sorted は最後に寄せる。
  • 巨大入力でメモリ増大:
    • 回避: distinct/sorted は全体把握が必要。まず filter で減らす、必要なら並列化やチャンク処理を検討。

まとめ

  • filter: 要らないものを落とす。
  • map: 1→1変換。
  • flatMap: 1→複数や欠損スキップを“平坦化”して一列に。
  • 絞り込みを前段に、展開や重い処理は後段に置くと、速くて読みやすいパイプラインになる。練習として「文→単語抽出」「ユーザー→スキル一覧」を flatMap で書いてみると、違いが直感で掴めます。
タイトルとURLをコピーしました