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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?