Java | Java 詳細・モダン文法:Stream API 深掘り – flatMap の用途

Java Java
スポンサーリンク

flatMap を一言でいうと

Stream#flatMap は、
「1 つの要素から“0 個以上の要素の流れ(Stream)”を作り、それらを 1 本のストリームに“平らに”つなげる中間操作」
です。

map が「1 対 1 の変換(T → R)」なら、
flatMap は「1 対 多の変換(T → Stream<R>)」を担当します。

「ネストした構造(リストの中のリスト、オブジェクトの中のリスト)を、1 本の流れにしたい」ときに真価を発揮します。


まずは map との違いから押さえる

map と flatMap のイメージの違い

map はこうです。

  • 入力:Stream<T>
  • 変換:T -> R
  • 出力:Stream<R>

flatMap はこうです。

  • 入力:Stream<T>
  • 変換:T -> Stream<R>
  • 出力:Stream<R>(ネストを“平らに”したもの)

もし flatMap がなかったら、map でこうなります。

Stream<Stream<R>>  // ストリームのストリーム(ネスト)
Java

flatMap は、この「ストリームのストリーム」を“平らにして 1 本にする”ための操作です。


flatMap のシグネチャと役割

Function と flatMap の関係

flatMap のシグネチャはこうです。

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

map との違いは、Function の戻り値が R ではなく Stream<R> になっているところです。

つまり、

  • map に渡す関数:T -> R
  • flatMap に渡す関数:T -> Stream<R>

flatMap は、

「各要素 T から Stream<R> を 1 本ずつ作り、それらを全部つなげて 1 本の Stream<R> にする」

という動きをします。


一番典型的な用途:リストの中のリストを 1 本にする

例:List<List<String>> を List<String> に“平らにする”

import java.util.List;

public class FlatMapBasic {
    public static void main(String[] args) {
        List<List<String>> list = List.of(
                List.of("A", "B"),
                List.of("C", "D", "E")
        );

        List<String> flat =
                list.stream()              // Stream<List<String>>
                    .flatMap(inner -> inner.stream()) // Stream<String>
                    .toList();

        System.out.println(flat); // [A, B, C, D, E]
    }
}
Java

ここで起きていることを分解すると:

  • 元のストリームの要素は List<String>
  • inner -> inner.stream() で、各 List<String>Stream<String> に変換
  • flatMap が、それぞれの Stream<String> を全部つなげて 1 本の Stream<String> にする

もし map を使うとこうなります。

list.stream()
    .map(inner -> inner.stream()); // Stream<Stream<String>>

このままだと「ストリームのストリーム」で扱いづらいので、
flatMap で“平らにする”わけです。


「オブジェクトの中のコレクション」を扱う用途

例:User が複数のメールアドレスを持っている場合

import java.util.List;

class User {
    private final String name;
    private final List<String> emails;
    User(String name, List<String> emails) {
        this.name = name;
        this.emails = emails;
    }
    String getName() { return name; }
    List<String> getEmails() { return emails; }
}

public class FlatMapUserExample {
    public static void main(String[] args) {
        List<User> users = List.of(
                new User("Alice", List.of("alice@example.com", "alice@work.com")),
                new User("Bob", List.of("bob@example.com")),
                new User("Charlie", List.of())
        );

        List<String> allEmails =
                users.stream()                         // Stream<User>
                     .flatMap(u -> u.getEmails().stream()) // Stream<String>
                     .toList();

        System.out.println(allEmails);
        // [alice@example.com, alice@work.com, bob@example.com]
    }
}
Java

ここでの流れはこうです。

  • Stream<User> からスタート
  • User について u.getEmails().stream() を呼ぶ → Stream<String>
  • flatMap がそれらを全部つなげて Stream<String> にする

「ユーザーのリスト」から「メールアドレスのリスト」を取り出したい、
というような「ネスト構造の展開」は、flatMap の王道パターンです。


「文字列 → 単語」のような 1 対 多変換

例:文章のリストから、すべての単語のリストを作る

import java.util.Arrays;
import java.util.List;

public class FlatMapWordsExample {
    public static void main(String[] args) {
        List<String> lines = List.of(
                "hello world",
                "java stream flatMap",
                "hello lambda"
        );

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

        System.out.println(words);
        // [hello, world, java, stream, flatMap, hello, lambda]
    }
}
Java

ここでの変換は、

  • 1 行(String) → 複数の単語(String の配列)
  • さらに Arrays.stream(...)Stream<String> に変換

という「1 対 多」の変換です。

map だと Stream<String[]>Stream<Stream<String>> になってしまいますが、
flatMap を使うことで「すべての単語が 1 本のストリームに流れてくる」形にできます。


flatMap の設計で意識したいポイント

「本当に flatMap が必要か?」をまず考える

flatMap を使うべき典型パターンは、ざっくり言うと次の 2 つです。

  • 要素が「コレクション(List など)」を持っていて、それを全部まとめて扱いたいとき
  • 1 つの要素から「0 個以上の要素」を生み出したいとき(文字列 → 単語など)

逆に、単純な「1 対 1 の変換」なら、
素直に map を使うべきです。

// これは map で十分
users.stream()
     .map(User::getName)
     .toList();
Java

flatMap を使うときは、
「変換結果が Stream(または配列・コレクション)になっていて、それを“平らにしたい”か?」
を自分に問いかけてください。

「変換の責務」と「平らにする責務」を分けて考える

flatMap の中で、つい色々やりたくなりますが、
設計としてはこう分けて考えるとスッキリします。

  • 変換:T -> Stream<R> をどう書くか
  • 平らにする:それを flatMap に任せる

例えば、User → メールアドレスの例なら、

private static Stream<String> emailsOf(User u) {
    return u.getEmails().stream();
}

users.stream()
     .flatMap(FlatMapUserExample::emailsOf)
     .toList();
Java

と分けることで、

「flatMap は“平らにする”だけ」
「具体的な変換は別メソッド」

という構造にできます。


flatMap と Optional の組み合わせ(少しだけ応用)

Optional.flatMap のイメージ

Optional にも flatMap がありますが、
考え方は Stream と同じです。

  • mapT -> U を適用して Optional<U> を返す
  • flatMapT -> Optional<U> を適用して、そのまま“平らに”する

例えば、

Optional<User> findUser(...);

Optional<String> email =
        findUser(...)
            .flatMap(u -> u.getMainEmail()); // getMainEmail が Optional<String> を返す
Java

「ネストした Optional を平らにする」という用途で使われます。

Stream の flatMap に慣れておくと、Optional の flatMap も理解しやすくなります。


まとめ:flatMap の用途を自分の言葉で整理する

flatMap をあなたの言葉でまとめるなら、

「1 つの要素から“0 個以上の要素の流れ”を作り、それらを全部つなげて 1 本のストリームにする中間操作」
です。

特に意識しておきたいのは、

  • map は「1 対 1」、flatMap は「1 対 多」
  • 「リストの中のリスト」「オブジェクトの中のリスト」を“平らにしたい”ときが典型的な用途
  • 変換は T -> Stream<R> として設計し、“平らにする”のは flatMap に任せる
  • 文字列 → 単語、ユーザー → メールアドレス、グループ → メンバー、などでよく使う

という点です。

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