はじめに (対象読者・この記事でわかること)

この記事は、Javaでコレクションを扱う際に「List>からListへフラットにしたい」というニーズに直面した方を対象としています。特に、Stream APIを使った現代的な書き方と、従来のfor文での書き方、そしてApache Commonsなどのライブラリを活用した書き方を比較したい方に最適です。
記事を読み終えると、3つの異なるアプローチでネストしたリストをフラット化できるようになり、パフォーマンスや可読性の観点から最適な手段を選べるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法(ジェネリクス、拡張for文) - ListインターフェースとArrayListクラスの使い方 - Java 8以降のラムダ式とStream APIの基礎

なぜ「フラット化」が必要なのか

CSVの行ごとのデータを「List」として保持し、それをさらに「List>」で全行管理するようなケースを考えてみましょう。最終的に「全てのセルの値を一列に並べたい」といった要望が発生した場合、ネストした構造をフラットにする必要があります。
このような「ネストしたコレクションを一段にする」操作は、データ変換の基本中の基本です。Java標準のAPIだけでも実装は可能ですが、サードパーティライブラリを使うことでより簡潔に記述できます。本記事では、それぞれの手法のメリット・デメリットを踏まえて解説します。

3つの方法で実装して比較する

ここからは、実際に「List>」を「List」へ変換する3つの方法を実装してみます。
環境はJava 17を想定し、JUnit 5で動作確認できるようにコードを記載します。

方法1:Stream APIを使ったフラット化(Java 8以降)

Java 8で導入されたStream APIなら、flatMapという中間操作を使うことでネストを解消できます。
flatMapは「ストリームのストリーム」を「一つのストリーム」にフラットにしてくれるメソッドです。

Java
List<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つです。

  1. stream()で外側のリストをストリーム化
  2. flatMapで内側のリストを展開
  3. collectで結果をListとして取得

ワンライナーで書けるため、可読性も高く、並列化も容易です。

方法2:拡張for文で素直に展開する

Java 8以前の環境や、わざわざStreamを使いたくない場合は、シンプルにfor文で回す方法もあります。

Java
List<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つあります。

  1. filter(Objects::nonNull)で事前に除去
  2. 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セーフティを意識した実装の重要性を学んでいただけたかと思います。
次回は、同様の操作をSetMapに応用した高度な集約処理について掘り下げていく予定です。

参考資料