はじめに (対象読者・この記事でわかること)
この記事は、Javaでコレクションを扱う際に「List>からList
記事を読み終えると、3つの異なるアプローチでネストしたリストをフラット化できるようになり、パフォーマンスや可読性の観点から最適な手段を選べるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法(ジェネリクス、拡張for文) - ListインターフェースとArrayListクラスの使い方 - Java 8以降のラムダ式とStream APIの基礎
なぜ「フラット化」が必要なのか
CSVの行ごとのデータを「List>」で全行管理するようなケースを考えてみましょう。最終的に「全てのセルの値を一列に並べたい」といった要望が発生した場合、ネストした構造をフラットにする必要があります。
このような「ネストしたコレクションを一段にする」操作は、データ変換の基本中の基本です。Java標準のAPIだけでも実装は可能ですが、サードパーティライブラリを使うことでより簡潔に記述できます。本記事では、それぞれの手法のメリット・デメリットを踏まえて解説します。
3つの方法で実装して比較する
ここからは、実際に「List>」を「List
環境はJava 17を想定し、JUnit 5で動作確認できるようにコードを記載します。
方法1:Stream APIを使ったフラット化(Java 8以降)
Java 8で導入されたStream APIなら、flatMapという中間操作を使うことでネストを解消できます。
flatMapは「ストリームのストリーム」を「一つのストリーム」にフラットにしてくれるメソッドです。
JavaList<List<String>> nested = List.of( List.of("A", "B"), List.of("C", "D", "E") ); List<String> flat = nested.stream() .flatMap(List::stream) // 各List<String>を展開 .collect(Collectors.toList()); System.out.println(flat); // [A, B, C, D, E]
このコードのポイントは3つです。
stream()で外側のリストをストリーム化flatMapで内側のリストを展開collectで結果をListとして取得
ワンライナーで書けるため、可読性も高く、並列化も容易です。
方法2:拡張for文で素直に展開する
Java 8以前の環境や、わざわざStreamを使いたくない場合は、シンプルにfor文で回す方法もあります。
JavaList<String> flat = new ArrayList<>(); for (List<String> inner : nested) { flat.addAll(inner); }
addAllを使うことで、内側のリストを一気に追加できます。
この書き方の利点は、Java 8未満の古い環境でも動作することと、デバッグ時にブレークポイントを張りやすいことです。
一方で、並列化が困難であり、中間処理を挟みにくいという欠点もあります。
方法3:Apache Commons Collectionsの「ListUtils」でラクラク解決
プロジェクトにApache Commons Collectionsが既に依存している場合、ListUtils.unionを連鎖させることで同様の処理を実現できます。
※ちなみに、unionは可変長引数を取れないため、reduceで連結するイディオムを使います。
Java// build.gradle (Gradle) implementation 'org.apache.commons:commons-collections4:4.4' // コード例 List<String> flat = nested.stream() .reduce(new ArrayList<>(), (acc, list) -> ListUtils.union(acc, list));
Apache Commonsを使うメリットは、ライブラリが提供するユーティリティを最大限活用できることです。
しかし、外部ライブラリを追加するほどのことでもない、という場合も多いでしょう。
パフォーマンスと可読性の観点から
100万要素を超えるような大規模データを扱う場合、Stream APIよりもfor文の方がわずかに高速です。
しかし現実的には、可読性と保守性を優先してStream APIを選ぶケースが多いです。
また、Apache Commonsを既に依存に含んでいる場合はListUtilsを使うことで、他の開発者にとっても意図が伝わりやすくなります。
よくあるエラー:NullPointerExceptionに注意
実務では、ネストしたリストの途中にnullが混入していることがあります。
以下のようにnullチェックを忘れると、ごく簡単にNullPointerExceptionが発生します。
Java// 悪い例:nullセーフティを考慮していない List<String> flat = nested.stream() .flatMap(List::stream) // ここでnullリストがあるとNPE .collect(Collectors.toList());
回避策は2つあります。
filter(Objects::nonNull)で事前に除去Optional.ofNullableでラップしてからflatMap
Java// 安全な例 List<String> flatSafe = nested.stream() .filter(Objects::nonNull) .flatMap(inner -> inner.stream()) .collect(Collectors.toList());
解決策
結論として、 - Java 8以降の環境ではStream API + flatMapがデファクトスタンダード - 古い環境や極端にパフォーマンスを求められる場合はfor文 + addAll - 既存プロジェクトでApache Commons Collectionsが入っているならListUtils.union
のいずれかを選ぶとよいでしょう。
重要なのは、チームのコーディング規約と処理対象データのサイズを見極めた上で、一貫性を持って使い分けることです。
まとめ
本記事では、JavaでList<List<String>>をList<String>へフラットにする3つの方法を紹介しました。
- Stream APIを使ったモダンで可読性の高い手法
- 拡張for文を使った古典的だが確実な手法
- Apache Commons Collectionsを使ったユーティリティ依存の手法
この記事を通して、シチュエーションに応じて最適なフラット化手法を選択できるようになり、さらにはnullセーフティを意識した実装の重要性を学んでいただけたかと思います。
次回は、同様の操作をSetやMapに応用した高度な集約処理について掘り下げていく予定です。
参考資料
- Java Platform, Standard Edition 17 Stream API Documentation
- Apache Commons Collections 4.4 ListUtils Javadoc
- Baeldung - Java Flatten Nested List
