【Java】Java Stream APIの使い方を基本から解説

Java

こんにちは、かつコーチです。
Listをfor文でループしながらif文で絞り込んで、さらに別のListに詰め替える…という処理を書いて、コードが長くなった経験はありませんか。
今回はJava 8で導入されたStream API(コレクションの要素を一連の処理でまとめて扱う仕組み)の基本を解説します。

Stream APIとは?

なぜStream APIが必要なのか

例えば「Listの中から偶数だけを取り出して2倍にする」処理を、従来のfor文で書くと次のようになります。

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> result = new ArrayList<>();

for (int n : numbers) {
    if (n % 2 == 0) {
        result.add(n * 2);
    }
}
System.out.println(result); // [4, 8, 12]

「何をするか」よりも「どうループするか」に多くの行数を使っています。
Stream APIを使うと、この処理を次のように書けます。

import java.util.List;
import java.util.stream.Collectors;

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> result = numbers.stream()
        .filter(n -> n % 2 == 0)
        .map(n -> n * 2)
        .collect(Collectors.toList());

System.out.println(result); // [4, 8, 12]

「偶数を選ぶ」「2倍にする」「Listに集める」という処理の流れがそのままコードに現れており、宣言的(手順ではなく目的を書くスタイル)に読めます。

Streamの3つのステップ

Stream処理は必ず次の3ステップで構成されます。

  1. 生成:ListなどからStreamを作る(stream()
  2. 中間操作:フィルタや変換など、Streamを加工する(filtermapなど)
  3. 終端操作:最終的な結果を取り出す(collectforEachなど)

中間操作は何回でもつなげられますが、終端操作を呼ぶまでは実際の処理は実行されません(遅延評価)。

基本の書き方

Listから作る基本パターン

import java.util.List;

List<String> names = List.of("かつコーチ", "Aさん", "Bさん", "Cさん");

names.stream()
        .filter(name -> name.length() >= 3)
        .forEach(System.out::println); // かつコーチ

stream()でStreamに変換し、filterで条件に合う要素だけを残し、forEachで1つずつ処理しています。

配列や数値範囲から作る

import java.util.Arrays;
import java.util.stream.IntStream;

int[] scores = {80, 92, 65, 78};
int total = Arrays.stream(scores).sum();
System.out.println(total); // 315

IntStream.range(1, 5).forEach(System.out::println); // 1,2,3,4

配列はArrays.stream()、連番はIntStream.range()のように、Listだけでなく様々な入力からStreamを作れます。
IntStreamは数値専用のStreamで、sum()average()など数値集計のメソッドが使える点が特徴です。

よくあるつまずきポイント・エラー対処

同じStreamを2回使おうとしてエラーになった

実際に私がハマった経験ですが、1つのStreamを2回終端操作にかけようとして実行時エラーになったことがあります。

// ❌Before:一度使ったStreamを再利用しようとする
List<Integer> numbers = List.of(1, 2, 3);
var stream = numbers.stream();

long count = stream.count();
stream.forEach(System.out::println); // 実行時エラー

このコードは java.lang.IllegalStateException: stream has already been operated upon or closed になります。
Streamは一度きりの使い捨てで、終端操作を1回実行すると再利用できません。

// ✅After:使うたびに新しいStreamを生成する
List<Integer> numbers = List.of(1, 2, 3);

long count = numbers.stream().count();
numbers.stream().forEach(System.out::println);

Streamを変数に保存して使い回すのではなく、必要になるたびにstream()を呼び直すのが正しい使い方です。

応用・一歩先の使い方

メソッドチェーンで複数の処理をつなげる

List<String> names = List.of("かつコーチ", "Aさん", "Bさん", "Cさん");

long count = names.stream()
        .filter(name -> !name.startsWith("A"))
        .map(String::toUpperCase)
        .distinct()
        .count();

System.out.println(count); // 3

filtermapdistinctのように中間操作を数珠つなぎにできるのがStream APIの真骨頂です。
次回以降の記事で、この中間操作と終端操作それぞれをさらに詳しく解説します。

forループとStreamの使い分け

単純な繰り返しやループの途中でbreakしたい場合はfor文の方が適しています。
一方、「フィルタ・変換・集計」を組み合わせる処理はStream APIの方が簡潔で読みやすくなる傾向があります。
どちらが優れているというより、処理の性質に応じて使い分けるのが実務的な判断です。

まとめ

この記事のポイント

  • Stream APIはコレクションの要素を「生成・中間操作・終端操作」の3ステップで宣言的に処理する仕組み
  • 中間操作(filtermapなど)は終端操作を呼ぶまで実行されない(遅延評価)
  • Streamは一度きりの使い捨てで、同じStreamを2回終端操作にかけるとIllegalStateExceptionになる
  • 単純なループはfor文、フィルタ・変換・集計の組み合わせはStream APIが向いている

次に読むべき記事

  • 中間操作(filter・map・sorted)
  • 終端操作(collect・reduce)

タグ: Java, 中級者向け, 関数型プログラミング

タイトルとURLをコピーしました