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]
Javamap(変換)
List<String> words = List.of("java", "stream");
List<Integer> lengths = words.stream()
.map(String::length)
.toList(); // [4,6]
JavaflatMap(展開+平坦化)
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 で書いてみると、違いが直感で掴めます。
