JavaのflatMapメソッドを徹底解説!Stream APIでListの入れ子を平坦化する方法
生徒
「JavaのStream APIを勉強中なのですが、リストの中にリストが入っているような複雑な構造を扱うのが難しくて困っています。」
先生
「それは『入れ子構造』や『二重リスト』と呼ばれるものですね。JavaではflatMapメソッドを使うことで、それらを一つの平らなリストにまとめることができるんですよ。」
生徒
「flatMapですか?mapメソッドとは何が違うんでしょうか?」
先生
「鋭いですね。mapは要素を変換するだけですが、flatMapは変換した後にそれらを結合して平らにする役割があります。具体的な例を見ていきましょう!」
1. flatMapメソッドの基本的な役割とは?
JavaのStream APIにおいて、flatMapメソッドは非常に強力なツールです。一言で言えば、「複数の要素を持つストリームの各要素を、それぞれ別のストリームに変換し、最終的にそれらを一つのストリームに連結する」という操作を行います。これをプログラミング用語で「平坦化(Flattening)」と呼びます。
例えば、ある学校に複数のクラスがあり、それぞれのクラスが学生のリストを持っているとします。全校生徒のリストを作りたい場合、各クラスのリストを取り出して、それらをすべてつなぎ合わせる必要があります。このような処理を、ループを使わずにスマートに記述できるのがflatMapの最大のメリットです。
初心者の方が特につまずきやすいのが、List<List<String>>のような二重構造のデータです。通常のmapメソッドでは、要素を変換するだけで構造は二重のままですが、flatMapを使うことで中身を外に引き出して一列に並べ直すことができます。これにより、データの加工や集計が格段に楽になります。
2. mapメソッドとflatMapメソッドの違いを理解する
flatMapを理解する上で、最も重要なのがmapメソッドとの比較です。この二つの違いを明確にすることで、どのような場面でどちらを使うべきかが判断できるようになります。
mapメソッドは、「一対一」の変換を行います。例えば、文字列のリストをすべて大文字に変換する場合、入力が3個なら出力も3個です。一方で、flatMapは「一対多(または一対ゼロ)」の変換を行います。一つの要素から複数の要素を生成し、それらを一つのストリームに統合します。
具体例を挙げると、mapでリストのリストを処理すると、結果も「リストのリスト」になります。しかし、flatMapで処理すると、中の要素がすべて取り出されて「一つの大きなリスト」になります。この「包み紙を破って中身を取り出すイメージ」が、flatMapを使いこなすコツです。
3. Listの入れ子を平坦化する具体的なプログラム例
それでは、実際にList<List<String>>という二重リストを、一つのList<String>に変換するコードを見てみましょう。これが最も標準的なflatMapの使い方です。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FlatMapBasicExample {
public static void main(String[] args) {
// 二重構造のリスト(リストの中にリストがある)
List<List<String>> nestedList = Arrays.asList(
Arrays.asList("リンゴ", "バナナ"),
Arrays.asList("イチゴ", "メロン"),
Arrays.asList("ブドウ")
);
// flatMapを使って平坦化
List<String> flatList = nestedList.stream()
.flatMap(list -> list.stream()) // 各リストをストリームに変換して結合
.collect(Collectors.toList());
System.out.println("元のリスト: " + nestedList);
System.out.println("平坦化後のリスト: " + flatList);
}
}
上記のコードの実行結果は以下のようになります。
元のリスト: [[リンゴ, バナナ], [イチゴ, メロン], [ブドウ]]
平坦化後のリスト: [リンゴ, バナナ, イチゴ, メロン, ブドウ]
コード内のflatMap(list -> list.stream())の部分がポイントです。外側のストリームから取り出された各子リスト(list)に対して、さらにstream()を呼び出すことで、中の要素を順番にメインのストリームへ流し込んでいます。
4. 文字列を分割して単語単位のリストを作る応用例
次に、文章のリストから単語を切り出し、重複のない単語リストを作成する例を紹介します。これはログ解析やテキストマイニングなどで非常によく使われるパターンです。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StringSplitExample {
public static void main(String[] args) {
List<String> sentences = Arrays.asList(
"Java is fun",
"Stream API is powerful",
"Learn Java programming"
);
// スペースで分割して平坦化し、ユニークな単語を取得
List<String> uniqueWords = sentences.stream()
.map(sentence -> sentence.split(" ")) // 文字列を配列に分割
.flatMap(Arrays::stream) // 配列をストリームにして結合
.distinct() // 重複を除去
.sorted() // アルファベット順にソート
.collect(Collectors.toList());
System.out.println("抽出された単語: " + uniqueWords);
}
}
実行結果は以下のようになります。
抽出された単語: [API, Java, Learn, Stream, fun, is, n, powerful, programming]
ここでは、まずmapで一文を一組の配列(String[])に変換し、その後flatMapを使ってバラバラの単語を一列に並べています。このようにmapとflatMapを組み合わせて使うことも多いです。
5. クラスのフィールドに含まれるリストを抽出する
実際の開発現場では、エンティティ(オブジェクト)のリストから、その中に保持されている子要素のリストを取り出したい場面が多々あります。例えば、「注文」オブジェクトの中に含まれる「注文明細」をすべて取り出すようなケースです。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class Order {
private String id;
private List<String> items;
public Order(String id, List<String> items) {
this.id = id;
this.items = items;
}
public List<String> getItems() {
return items;
}
}
public class ObjectFlatMapExample {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("101", Arrays.asList("PC", "Mouse")),
new Order("102", Arrays.asList("Keyboard")),
new Order("103", Arrays.asList("Monitor", "Cable"))
);
// 全注文の中から商品名だけをすべて抜き出す
List<String> allItems = orders.stream()
.flatMap(order -> order.getItems().stream())
.collect(Collectors.toList());
System.out.println("全注文商品リスト: " + allItems);
}
}
実行結果は以下のようになります。
全注文商品リスト: [PC, Mouse, Keyboard, Monitor, Cable]
このように、オブジェクトの階層構造を突き抜けて特定のデータだけを収集する際に、flatMapは非常にスマートな解決策を提供してくれます。従来のfor文であれば二重ループを書く必要がありましたが、Stream APIなら数行で記述可能です。
6. 数値の配列を扱うflatMapToIntなどの特化型
JavaのStream APIには、プリミティブ型(int, long, double)に特化したストリームも存在します。これらに対応するflatMapToIntやflatMapToLongなどを使うと、ボクシング(型変換)のオーバーヘッドを減らし、パフォーマンスを向上させることができます。
import java.util.Arrays;
import java.util.stream.IntStream;
public class PrimitiveFlatMapExample {
public static void main(String[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5},
{6, 7, 8, 9}
};
// 二次元配列を平坦化して合計を計算
int sum = Arrays.stream(matrix)
.flatMapToInt(row -> Arrays.stream(row))
.sum();
System.out.println("全要素の合計値: " + sum);
// 最大値の取得なども簡単
int max = Arrays.stream(matrix)
.flatMapToInt(Arrays::stream)
.max()
.orElse(0);
System.out.println("最大値: " + max);
}
}
実行結果は以下のようになります。
全要素の合計値: 45
最大値: 9
二次元配列は実質的に「配列の配列」ですので、これも立派な入れ子構造です。データ分析や画像処理などの計算処理において、多次元データを一次元に直して一気に計算したい場合に、これらのメソッドは非常に役立ちます。
7. flatMapを使う際の注意点と使い分け
非常に便利なflatMapですが、使用する際にいくつか注意すべき点があります。まず一つ目は、「Nullチェック」です。flatMapに渡す関数がnullを返すとNullPointerExceptionが発生する可能性があります。ストリーム化する前に、中身が空でないか、あるいはOptionalを適切に扱っているかを確認しましょう。
二つ目は、可読性の問題です。あまりに入れ子が深い構造に対してflatMapを多用しすぎると、何をやっているコードなのか一目で分かりにくくなることがあります。必要に応じて、ラムダ式をメソッド参照に書き換えたり、処理を分割して変数に意味のある名前をつけたり工夫しましょう。
また、flatMapは「空のストリーム」を返すことで、要素を除去するフィルタリングのような役割も果たせます。例えば、特定の条件を満たさない場合にStream.empty()を返すようにすれば、変換と除外を同時に行うことができます。これはfilterとmapを個別に呼ぶよりも効率的な場合があります。
8. OptionalクラスとflatMapの関係
実は、flatMapはStreamクラスだけでなく、Optionalクラスにも存在します。これはJava 8以降のプログラミングで非常に重要な概念です。Optionalの中にさらに関数の戻り値としてOptionalが返ってくる場合、そのままmapを使うとOptional<Optional<String>>という非常に扱いにくい型になってしまいます。
ここでflatMapを使うと、外側のOptionalと内側のOptionalを一つにまとめてくれます。これにより、値が存在する場合のみ次の処理へ進む、という一連の流れを「メソッドチェーン」で安全に記述できるのです。ネストしたif (obj != null)の記述を排除できるため、コードが非常にスッキリします。
Stream APIのflatMapと同様に、「階層を一段階下げる」という共通のイメージを持っておくと、OptionalのflatMapもスムーズに理解できるはずです。このようにJavaのモダンな機能は、共通の設計思想で作られていることが多いので、一つをマスターすれば他の理解も早まります。
9. 実践!より複雑なデータ構造のハンドリング
最後に応用として、地図データのような階層構造を扱ってみましょう。例えば「国」の中に「都市」があり、それぞれの都市に「観光名所」があるようなデータから、全観光名所の名前を抽出する処理です。ここまで学習したあなたなら、どのように実装すればよいかイメージが湧くはずです。
階層が三段階以上になっても、flatMapを繋げるだけで対応可能です。countries.stream().flatMap(c -> c.getCities().stream()).flatMap(city -> city.getSpots().stream())といった具合です。このように、深い階層にあるデータへアクセスする際の「ショートカット」としても、flatMapは最強の武器になります。
これまで二重ループや三重ループを使って苦労して書いていた処理が、驚くほど簡潔になる感動をぜひ体験してください。Javaのエンジニアとしてステップアップするためには、このflatMapを自由自在に操れるようになることが、一つの大きな関門であり、醍醐味でもあります。