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();
Javamapped の中身はこういうイメージになります。
["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)
JavaT を受け取って R を返す関数を渡す。
ストリームとしては
Stream<T> → map → Stream<R>
です。
flatMap の型
flatMap はこうです。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Java少しゴツいですが、本質だけ拾うと、
「T を受け取って Stream<R> を返す関数」を渡す
ということです。
ストリームとしては、
Stream<T> → flatMap → Stream<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
}
}
Javaclasses の型は 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]
Javaline.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 本のストリームにしたいときに気持ちよくハマる
