こんにちは、かつコーチです。
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ステップで構成されます。
- 生成:ListなどからStreamを作る(
stream()) - 中間操作:フィルタや変換など、Streamを加工する(
filter・mapなど) - 終端操作:最終的な結果を取り出す(
collect・forEachなど)
中間操作は何回でもつなげられますが、終端操作を呼ぶまでは実際の処理は実行されません(遅延評価)。
基本の書き方
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
filter・map・distinctのように中間操作を数珠つなぎにできるのがStream APIの真骨頂です。
次回以降の記事で、この中間操作と終端操作それぞれをさらに詳しく解説します。
forループとStreamの使い分け
単純な繰り返しやループの途中でbreakしたい場合はfor文の方が適しています。
一方、「フィルタ・変換・集計」を組み合わせる処理はStream APIの方が簡潔で読みやすくなる傾向があります。
どちらが優れているというより、処理の性質に応じて使い分けるのが実務的な判断です。
まとめ
この記事のポイント
- Stream APIはコレクションの要素を「生成・中間操作・終端操作」の3ステップで宣言的に処理する仕組み
- 中間操作(
filter・mapなど)は終端操作を呼ぶまで実行されない(遅延評価) - Streamは一度きりの使い捨てで、同じStreamを2回終端操作にかけると
IllegalStateExceptionになる - 単純なループはfor文、フィルタ・変換・集計の組み合わせはStream APIが向いている
次に読むべき記事
- 中間操作(filter・map・sorted)
- 終端操作(collect・reduce)
タグ: Java, 中級者向け, 関数型プログラミング