Java 8 Stream APIによる効率的なコレクション操作と宣言的プログラミング

Java 8で導入されたStream APIは、データの集合(コレクションや配列など)を関数型プログラミングのスタイルで処理するための強力なインターフェースです。従来のforループによる命令的な記述とは異なり、「何をするか」を宣言的に記述できるため、コードの可読性と保守性が大幅に向上します。

Stream APIの基本概念

Streamはデータのパイプラインのようなもので、以下の3つの原則に基づいて動作します。

  • データを保持しない: Stream自体はデータ構造ではなく、データソース(コレクション、配列など)から要素を運ぶチャネルです。
  • 非破壊的: 元のデータソースを変更することはありません。操作の結果として新しいStreamや値を返します。
  • 遅延実行: 中間操作は即座に実行されず、結果が必要となる「終端操作」が呼び出されたタイミングで初めて実行されます。

Stream処理の3段階構成

Stream APIを利用した処理は、以下の3つのステップで構成されます。

  1. 生成 (Creation): データソースからStreamインスタンスを作成します。
  2. 中間操作 (Intermediate Operations): フィルタリングやマッピングなど、Streamを別のStreamに変換します。連結が可能です。
  3. 終端操作 (Terminal Operations): 結果を計算したり、リストに収集したりします。この操作によってStreamは消費され、再利用できなくなります。

1. Streamの生成方法

さまざまなデータソースからストリームを作成できます。

// リストから作成
List<String> deviceList = Arrays.asList("Laptop", "Mouse", "Keyboard");
Stream<String> deviceStream = deviceList.stream();

// 配列から作成
Integer[] priceArray = {100, 200, 300};
Stream<Integer> priceStream = Arrays.stream(priceArray);

// 静的メソッド of() を使用
Stream<String> colorStream = Stream.of("Red", "Green", "Blue");

// 無限ストリーム (反復生成)
Stream<Integer> incrementStream = Stream.iterate(0, n -> n + 2).limit(5); // 0, 2, 4, 6, 8

2. 主な中間操作

中間操作を組み合わせることで、複雑なデータ変換パイプラインを構築できます。

filter: 要素の抽出

条件に一致する要素のみを抽出します。

List<Integer> ages = Arrays.asList(18, 22, 15, 30);
ages.stream()
    .filter(age -> age >= 20)
    .forEach(System.out::println); // 22, 30

map: 要素の変換

各要素を別の形式に変換します。

List<String> names = Arrays.asList("java", "python", "go");
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println); // JAVA, PYTHON, GO

flatMap: ストリームの平坦化

要素がリストなどのコレクションである場合、それらを1つのストリームに統合します。

List<List<String>> complexList = Arrays.asList(
    Arrays.asList("A", "B"),
    Arrays.asList("C", "D")
);
complexList.stream()
           .flatMap(List::stream)
           .forEach(System.out::print); // ABCD

sorted / distinct: ソートと重複除去

List<Integer> nums = Arrays.asList(5, 2, 8, 2, 1);
nums.stream()
    .distinct()
    .sorted()
    .forEach(System.out::print); // 1258

3. 主な終端操作

終端操作を実行することで、パイプラインの処理が開始されます。

collect: 結果の収集

処理結果をListやSet、Mapなどにまとめます。

List<String> items = Arrays.asList("Apple", "Banana", "Cherry");
List<String> filteredItems = items.stream()
                                  .filter(s -> s.startsWith("A"))
                                  .collect(Collectors.toList());

reduce: 畳み込み操作

すべての要素を結合して1つの結果(合計値や最大値など)を算出します。

List<Integer> values = Arrays.asList(1, 2, 3, 4);
Optional<Integer> total = values.stream().reduce(Integer::sum);

anyMatch / allMatch / noneMatch: 判定操作

条件に合致するかどうかをbooleanで返します。

List<String> tags = Arrays.asList("web", "mobile", "cloud");
boolean hasCloud = tags.stream().anyMatch(t -> t.equals("cloud")); // true

Stream利用時の注意点

1. ストリームの再利用禁止

一度終端操作を行ったStreamインスタンスを再度使用しようとすると、IllegalStateExceptionが発生します。

Stream<String> s = Stream.of("once");
s.forEach(System.out::println);
// s.count(); // これは例外をスローする

2. ショートサーキットによる最適化

findFirstanyMatchなどの操作は、条件を満たした時点で残りの要素の処理を中断します。これにより、無限ストリームであっても有限の時間で処理を完了させることが可能です。

3. 並列ストリーム (Parallel Stream)

parallelStream()を使用すると、マルチコアプロセッサを活用して処理を並列化できます。ただし、共有リソースへのアクセスがある場合や、要素数が少ない場合は、逆にオーバーヘッドによって性能が低下する可能性があるため、慎重に検討する必要があります。

long count = largeList.parallelStream()
                      .filter(x -> x > 100)
                      .count();

タグ: Java Java8 StreamAPI FunctionalProgramming Backend

5月15日 10:15 投稿