Java | Java 標準ライブラリ:flatMap

Java Java
スポンサーリンク

flatMap をざっくりイメージする

まず、一番大事な感覚から。

map
「1つの要素 → 1つの値」に変換する操作でした。

flatMap
「1つの要素 → 0個以上の“複数要素”」に変換して、
最後にそれを「平ら(flat)」にまっすぐ 1 本のストリームに繋げる操作です。

少し具体的に言うと、

  • map は「T → R」を並べる
  • flatMap は「T → Stream<R>」を並べて、最後に全部つなげて 1 本にする

というイメージです。

例えるなら、

「クラスのリスト」から「各クラスの生徒のリスト」を取り出すときに
map だと「リストのリスト」になるけど、
flatMap だと「生徒を全部 1 本に並べたストリーム」になる、という感じです。


まず map と flatMap の違いを“カタチ”で感じる

例題:単語リストから「単語の文字配列」を作る

例えば、こんな単語リストがあるとします。

List<String> words = Arrays.asList("Java", "Stream");
Java

これを map で「1文字ずつの配列」に変換してみます。

List<String[]> mapped =
        words.stream()
             .map(word -> word.split(""))  // String -> String[]
             .toList();
Java

mapped の中身はこういうイメージになります。

  • ["J", "a", "v", "a"]
  • ["S", "t", "r", "e", "a", "m"]

つまり、型としては

List<String[]> または Stream<String[]>
Java

です。「配列のストリーム」になっている、という状態です。

やりたいのは「全部の文字を 1 本に並べる」場合

もしやりたいことが

「J, a, v, a, S, t, r, e, a, m」

と 1 本に並べたストリーム(または List)にしたい、だとします。

map のままだと「配列の集まり」になっているので、
そこで登場するのが flatMap です。

List<String> flattened =
        words.stream()
             .flatMap(word -> Arrays.stream(word.split("")))
             .toList();

System.out.println(flattened);
// [J, a, v, a, S, t, r, e, a, m]
Java

ここでやっていることは、

word から String[] を作るだけでなく
String[] から Stream<String> を作って
その「複数の Stream を全部つなげて 1 本にする」

という動きです。

型の流れで見ると、

Stream<String>(元)
flatMap word -> Stream<String>
Stream<String>(結果も 1 本の Stream<String>)

という感じで、「中間にあった Stream の入れ子構造が平らになる(flat)」から flatMap という名前になっています。


flatMap の「型の動き」を丁寧に追ってみる

map の型

map のシグネチャ(ざっくり)はこうでした。

<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Java

T を受け取って R を返す関数を渡す。
ストリームとしては

Stream<T>mapStream<R>

です。

flatMap の型

flatMap はこうです。

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Java

少しゴツいですが、本質だけ拾うと、

T を受け取って Stream<R> を返す関数」を渡す

ということです。

ストリームとしては、

Stream<T>flatMapStream<R>

ですが、中でやっているのは

1つの T から Stream<R> を作る
その「たくさんの Stream<R>」を全部つなげて 1 本の Stream<R> にする

という処理です。

つまり、

map → T → R
flatMap → T → Stream<R>(それを全部くっつけて R の Stream)

という違いだけ押さえれば十分です。


典型例:List<List<T>> を 1 本の List<T> にする

例題:クラスごとの生徒リストを「全員分」にしたい

例えば、「クラスごとの生徒リスト」があったとします。

import java.util.List;

class Student {
    String name;
    Student(String name) { this.name = name; }
    @Override public String toString() { return name; }
}

public class FlatMapStudents {
    public static void main(String[] args) {
        List<Student> classA = List.of(new Student("A1"), new Student("A2"));
        List<Student> classB = List.of(new Student("B1"), new Student("B2"), new Student("B3"));

        List<List<Student>> classes = List.of(classA, classB);

        // ここから flatMap
    }
}
Java

classes の型は List<List<Student>> です。

やりたいことは
「全クラスの生徒を 1 本の List<Student> にしたい」ですよね。

map だけだと「リストのリスト」が残る

まずは map で考えてみます。

List<List<Student>> mapped =
        classes.stream()
               .map(classList -> classList)  // そのまま
               .toList();
Java

これは何も変わりません。
ストリームの中身が List<Student> のままです。

もし、生徒を表示したくてこう書くとします。

classes.stream()
       .map(classList -> classList.stream()) // Stream<Student> の Stream になる
       .forEach(System.out::println);
Java

この場合 forEach に来るのは Stream<Student> なので、
Stream そのものが出力されてしまいます(中身の生徒ではない)。

flatMap で「中の List を全部つなげる」

ここで flatMap の出番です。

List<Student> allStudents =
        classes.stream()                     // Stream<List<Student>>
               .flatMap(classList -> classList.stream()) // Stream<Student> を返す
               .toList();                    // List<Student>
Java

流れを言葉で追うと、

classes.stream() で「クラスごとの List<Student>」が流れてくる
flatMap の中で、各クラスの List<Student>classList.stream() に変える
→ つまり、「各クラスの生徒ストリーム」が出てくる
flatMap がそれらを全部つなげて、「全員分が順に流れる 1 本の Stream<Student>」にする
最後に toList で集める

という動きです。

結果の allStudents には、

[A1, A2, B1, B2, B3]

という 1 本のリストが入ります。

「リストのリストを“平らにして 1 本にする”」
これが flatMap の典型中の典型です。


文字列処理の例:文章リスト → 単語リスト → 文字リスト

例題:複数行のテキストから、すべての単語を取り出す

こういうデータがあったとします。

List<String> lines = List.of(
        "Java is fun",
        "Stream makes code compact"
);
Java

やりたいことは、「すべての単語を 1 本の List<String> にする」。

例えば結果は、

[“Java”, “is”, “fun”, “Stream”, “makes”, “code”, “compact”]

のようにしたい。

map だけだと「単語配列のリスト」になる

まずは map を使ってみます。

List<String[]> mapped =
        lines.stream()
             .map(line -> line.split(" ")) // String -> String[]
             .toList();
Java

これは、

[“Java”, “is”, “fun”]
[“Stream”, “makes”, “code”, “compact”]

という「配列のリスト」です。
欲しいのは「単語のリスト」ですよね。

flatMap で「配列を 1 本につなげる」

List<String> words =
        lines.stream()
             .flatMap(line -> Arrays.stream(line.split(" ")))
             .toList();

System.out.println(words);
// [Java, is, fun, Stream, makes, code, compact]
Java

line.split(" ") で 1 行を単語配列にする
Arrays.stream(その配列)Stream<String> にする
flatMap が、「各行の単語ストリーム」を全部つなげて 1 本のストリームにする

という流れになっています。

このパターン、実務でもかなり頻出です。


flatMap の「0個以上」という感覚(フィルタ的な使い方)

「条件に合わないなら“空のストリーム”を返す」という書き方

flatMap の中で「空のストリーム」を返すと、その要素は“なかったもの”として扱われます。

例えば、「null や空文字を除外して、1文字ずつのストリームにしたい」という場合。

List<String> list = List.of("A", "", null, "BC");

List<String> chars =
        list.stream()
            .flatMap(s -> {
                if (s == null || s.isEmpty()) {
                    return Stream.empty();   // 要素 0 個として扱う
                }
                return Arrays.stream(s.split(""));
            })
            .toList();

System.out.println(chars);  // ["A", "B", "C"]
Java

ここでは、

s が null または空文字なら「要素 0 個」のストリーム
それ以外なら「文字列を分解した要素 N 個」のストリーム

を返しています。

flatMap は「1 → 何個でも(0 個も含む)」という変換なので、
filter 的な動き(0 個)と map 的な動き(変換して複数)を同時に表現できます。

ただし、処理が複雑になるので、

単純に絞るだけなら filter
変換だけなら map
「分解して複数にしたい」ときだけ flatMap

と分けたほうが読みやすいことが多いです。


flatMap をいつ使うか・どう意識すると迷わなくなるか

こんなときに flatMap を思い出す

次のような感覚になったら、flatMap の出番だと思ってください。

List<List<T>> を 1 本の List<T> にしたい
「行の集まり」から「単語の集まり」にしたい(ネストをなくしたい)
「1 要素が、0個〜複数の要素に展開される」ような変換をしたい

逆にいえば、

1 → 1 の変換なら map
1 → 0 or 1 の単純な「残す / 捨てる」なら filter
1 → 0〜複数で、ネストをつぶして 1 本にしたいなら flatMap

という整理です。

無理に使わないことも大事

flatMap は便利ですが、無理に使うと一気に読みづらくなります。

「本当に“ネストを平らにしたい”状況なのか?」
「map で十分ではないか?」

を自分に問いかけてから使うと、コードの意図がクリアになります。


まとめ:flatMap を自分の中でどう定着させるか

初心者向けに flatMap を一言でまとめると、

1 つの要素から“0個以上の要素の Stream”を作り、それらを全部つなげて 1 本にする操作

です。

特に意識しておきたいポイントは、

  • map は「1 → 1」、flatMap は「1 → 複数(0 個含む)」
  • Stream<T> に対して、「T → Stream<R>」の変換をして、それを平らにする
  • List<List<T>> から List<T> を作るときの“定番手段”
  • 文字列やコレクションを分解して 1 本のストリームにしたいときに気持ちよくハマる

タイトルとURLをコピーしました