Stream API: 関数型データ処理

filter/map/reduce でコレクションを宣言的に処理。

filter
条件に合う要素を抽出。
map
各要素を変換。
reduce
要素を集約。
ベルトコンベア (Conveyor Belt)

Stream APIは「工場のベルトコンベア」です。 **filter** = 不良品を弾く検員 **map** = 各製品を加工する機械 **reduce** = 最後に1つにまとめる ベルトに流れるデータを、宣言的に処理します。

なぜStream APIが必要か?

Stream APIは、コレクション(List、Setなど)を宣言的に処理するためのAPIです。従来のforループを使った命令的なコードでは、処理の意図が読み取りにくくなりがちです。Stream APIを使うと、filter(フィルタリング)、map(変換)、reduce(集約)などの操作を連鎖させて、処理の意図を明確に記述できます。また、並列処理(parallelStream)を簡単に利用できるため、大量データの処理も効率的に行えます。

いつ使うか?

Stream APIは、以下のような場面で使います: 1. **コレクションをフィルタリングしたいとき**: 特定の条件に合う要素だけを抽出したいとき。 2. **コレクションを変換したいとき**: 各要素を別の形式に変換したいとき。 3. **コレクションを集約したいとき**: 要素を1つの値にまとめたいとき(合計、平均など)。 4. **大量データを並列処理したいとき**: データ量が多く、処理時間を短縮したいとき。

実践テクニック

ストリームは一度しか使えない

Streamは「消費型」で、一度しか使えません。同じStreamを2回以上使おうと、IllegalStateExceptionが発生します。再利用したい場合は、toList()などでコレクションに変換してから、再度stream()を呼び出す必要があります。

Optionalと連携する

Streamの終端操作(findFirst、findAnyなど)はOptionalを返します。Optionalはnullチェックを強制できるため、より安全なコードになります。orElse()やorElseThrow()を使って、値がない場合のデフォルト値や例外処理を明示的に記述できます。

並列処理を慎重に使う

parallelStream()を使うと、並列処理が可能になりますが、スレッドセーフティを考慮する必要があります。共有リソースへのアクセスは適切に同期し、データ競合を防ぐ必要があります。また、並列処理のオーバーヘッドが大きいため、小規模データでは逆に遅くなる可能性があります。

Stream API

filter, map, reduce
import java.util.List;
import java.util.stream.*;
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter: 条件に合う要素を抽出
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.toList(); // [2, 4, 6, 8, 10]
// map: 各要素を変換
List<Integer> doubled = numbers.stream()
.map(n -> n * 2)
.toList(); // [2, 4, 6, ..., 20]
// reduce: 集約
int sum = numbers.stream()
.reduce(0, Integer::sum); // 55
// 連鎖
double average = numbers.stream()
.filter(n -> n > 5)
.mapToInt(Integer::intValue)
.average()
.orElse(0.0); // 8.0
実行結果
[2, 4, 6, 8, 10]\n55\n8.0
Bad
// ❌ Bad: 長いforループ
List<String> result = new ArrayList<>();
for (User u : users) {
if (u.getAge() > 20) {
result.add(u.getName().toUpperCase());
}
}
Good
// ✅ Good: Stream で宣言的に
List<String> result = users.stream()
.filter(u -> u.getAge() > 20)
.map(u -> u.getName().toUpperCase())
.toList();

パターン

collect, flatMap, parallel
// collect: 様々な形式に変換
Map<String, Integer> map = users.stream()
.collect(Collectors.toMap(
User::getName,
User::getAge
));
// グルーピング
Map<String, List<User>> byDept = users.stream()
.collect(Collectors.groupingBy(User::getDepartment));
// flatMap: ネストを平坦化
List<String> allTags = articles.stream()
.flatMap(a -> a.getTags().stream())
.distinct()
.toList();
// 並列処理
long count = numbers.parallelStream()
.filter(n -> isPrime(n))
.count();
// Optional との連携
users.stream()
.filter(u -> u.getId() == targetId)
.findFirst()
.orElseThrow(() -> new UserNotFoundException(targetId));
Tip: Stream は一度しか使えない。再利用不可。

合格ライン

filter/map/reduce を使える
Collectors.toMap を使える

参考リンク

演習課題

課題1: filter/map
Stream で filter と map を連鎖させてください。
課題2: collect
Collectors.toList と toMap を使ってください。

次のステップ