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>> // ストリームのストリーム(ネスト)
JavaflatMap は、この「ストリームのストリーム」を“平らにして 1 本にする”ための操作です。
flatMap のシグネチャと役割
Function と flatMap の関係
flatMap のシグネチャはこうです。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Javamap との違いは、Function の戻り値が R ではなく Stream<R> になっているところです。
つまり、
mapに渡す関数:T -> RflatMapに渡す関数: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();
JavaflatMap を使うときは、
「変換結果が 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 と同じです。
map:T -> Uを適用してOptional<U>を返すflatMap:T -> 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 に任せる - 文字列 → 単語、ユーザー → メールアドレス、グループ → メンバー、などでよく使う
という点です。
