タイトル: ストリームAPI
SEOタイトル: Java Stream API 完全ガイド
| この記事の要点 |
|
Stream API の基本
Java 8 で導入された java.util.stream は、コレクション (List / Set / Map) や配列に対して関数型スタイルで一連の処理を記述するための API です。for ループより簡潔に書け、並列化も容易です。
import java.util.*;
import java.util.stream.*;
List names = List.of("Alice", "Bob", "Carol", "Dave");
// 例1: 大文字化して List に
List upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// [ALICE, BOB, CAROL, DAVE]
// 例2: 3 文字以下のみ
List short_ = names.stream()
.filter(n -> n.length() <= 3)
.collect(Collectors.toList());
// [Bob]
// 例3: チェイン (filter + map + collect)
List result = names.stream()
.filter(n -> n.startsWith("A") || n.startsWith("C"))
.map(String::toUpperCase)
.collect(Collectors.toList());
// [ALICE, CAROL]
パイプラインの構造
Stream の処理は「Source → 中間操作 → 終端操作」の 3 段構成です。
| 段階 | 役割 | 例 |
|---|---|---|
| Source | Stream の生成 | list.stream() / Stream.of(1,2,3) / IntStream.range(0,10) |
| 中間操作 | 変換 (Stream → Stream)、複数チェイン可、遅延評価 | filter / map / sorted / distinct / limit |
| 終端操作 | 結果生成 (Stream → 値)、1 回だけ | collect / forEach / count / reduce / findFirst |
中間操作 (Intermediate Operations)
List nums = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter: 条件に合うものだけ
nums.stream().filter(n -> n % 2 == 0)
.forEach(System.out::println); // 2 4 6 8 10
// map: 変換
nums.stream().map(n -> n * 2)
.forEach(System.out::println); // 2 4 6 8 10 12 14 16 18 20
// flatMap: ネストを平坦化
List> nested = List.of(List.of(1,2), List.of(3,4), List.of(5));
nested.stream()
.flatMap(List::stream)
.forEach(System.out::println); // 1 2 3 4 5
// sorted: ソート
nums.stream().sorted(Comparator.reverseOrder())
.forEach(System.out::println); // 10 9 8 ... 1
// distinct: 重複除去
Stream.of(1, 2, 2, 3, 3, 3).distinct()
.forEach(System.out::println); // 1 2 3
// limit / skip
nums.stream().skip(3).limit(2)
.forEach(System.out::println); // 4 5
// peek: 途中で副作用 (デバッグ用途)
nums.stream()
.peek(n -> System.out.println("In: " + n))
.map(n -> n * 2)
.peek(n -> System.out.println("Out: " + n))
.collect(Collectors.toList());
終端操作 (Terminal Operations)
List nums = List.of(1, 2, 3, 4, 5);
// collect: 集約
List list = nums.stream().collect(Collectors.toList());
Set set = nums.stream().collect(Collectors.toSet());
String csv = nums.stream().map(String::valueOf)
.collect(Collectors.joining(",")); // "1,2,3,4,5"
// Java 16+ の toList() (短く書ける)
List list2 = nums.stream().toList();
// count
long count = nums.stream().filter(n -> n > 2).count(); // 3
// reduce: 集約値計算
int sum = nums.stream().reduce(0, Integer::sum); // 15
int max = nums.stream().reduce(Integer.MIN_VALUE, Integer::max); // 5
Optional sumOpt = nums.stream().reduce(Integer::sum); // Optional[15]
// findFirst / findAny
Optional first = nums.stream().filter(n -> n > 3).findFirst(); // Optional[4]
// anyMatch / allMatch / noneMatch
boolean hasEven = nums.stream().anyMatch(n -> n % 2 == 0); // true
boolean allPos = nums.stream().allMatch(n -> n > 0); // true
// forEach
nums.stream().forEach(System.out::println);
Collectors
import static java.util.stream.Collectors.*;
record Person(String name, String dept, int age) {}
List people = List.of(
new Person("Alice", "Sales", 25),
new Person("Bob", "Tech", 30),
new Person("Carol", "Sales", 28),
new Person("Dave", "Tech", 35)
);
// groupingBy: 部署ごとにグループ
Map> byDept = people.stream()
.collect(groupingBy(Person::dept));
// {Sales=[Alice, Carol], Tech=[Bob, Dave]}
// groupingBy + counting
Map countByDept = people.stream()
.collect(groupingBy(Person::dept, counting()));
// {Sales=2, Tech=2}
// groupingBy + averagingInt
Map avgAge = people.stream()
.collect(groupingBy(Person::dept, averagingInt(Person::age)));
// {Sales=26.5, Tech=32.5}
// partitioningBy: 二分割
Map> over30 = people.stream()
.collect(partitioningBy(p -> p.age() >= 30));
// {false=[Alice, Carol], true=[Bob, Dave]}
// toMap
Map nameToAge = people.stream()
.collect(toMap(Person::name, Person::age));
// {Alice=25, Bob=30, Carol=28, Dave=35}
Primitive Stream
オートボクシングを避けるため、プリミティブ型専用の Stream があります:
// IntStream / LongStream / DoubleStream
// 0〜9 の範囲
IntStream.range(0, 10).forEach(System.out::println);
// 0〜10 (10 含む)
IntStream.rangeClosed(0, 10).sum(); // 55
// 平均、合計、最大、最小、件数
IntSummaryStatistics stats = IntStream.of(1, 2, 3, 4, 5).summaryStatistics();
stats.getAverage(); // 3.0
stats.getSum(); // 15
stats.getMax(); // 5
// 通常の Stream に変換
Stream boxed = IntStream.range(0, 5).boxed();
// 逆変換
int[] arr = Stream.of(1, 2, 3).mapToInt(Integer::intValue).toArray();
遅延評価
Stream は終端操作が呼ばれるまで何も実行しません。中間操作を 100 個チェインしても、終端操作がなければゼロ動作です。
List names = List.of("Alice", "Bob", "Carol");
// 終端操作なし → 何も実行されない
names.stream()
.peek(n -> System.out.println("filter: " + n))
.filter(n -> n.startsWith("A"));
// 何も出力されない
// findFirst で短絡評価
names.stream()
.peek(n -> System.out.println("peek: " + n))
.filter(n -> n.startsWith("C"))
.findFirst();
// peek: Alice
// peek: Bob
// peek: Carol ← Carol で見つけたら処理停止
並列ストリーム (parallelStream)
// 大量データを並列処理
List nums = IntStream.range(0, 1_000_000).boxed().toList();
// シーケンシャル
long sum1 = nums.stream().mapToLong(Integer::longValue).sum();
// パラレル (ForkJoinPool.commonPool() で並列化)
long sum2 = nums.parallelStream().mapToLong(Integer::longValue).sum();
// ⚠ 注意: 副作用のある処理 (collect 以外) は競合する
List result = new ArrayList<>();
nums.parallelStream().forEach(result::add); // ❌ ArrayList はスレッドセーフでない
// ✅ 集約は Collectors を使う
List safe = nums.parallelStream().collect(Collectors.toList());
Stream は再利用不可
Stream s = Stream.of("a", "b", "c");
s.forEach(System.out::println); // ✅ a b c
s.forEach(System.out::println); // ❌ IllegalStateException: stream has already been operated upon
// 再利用したい → Supplier から都度生成
Supplier> sup = () -> Stream.of("a", "b", "c");
sup.get().forEach(System.out::println);
sup.get().count();
Java 9+ の追加
// takeWhile / dropWhile (Java 9+)
Stream.of(1, 2, 3, 4, 1, 2).takeWhile(n -> n < 4).forEach(System.out::println);
// 1 2 3 (4 で停止、以降は捨てる)
Stream.of(1, 2, 3, 4, 1, 2).dropWhile(n -> n < 4).forEach(System.out::println);
// 4 1 2 (条件不成立まで捨て、以降は残す)
// Optional.stream (Java 9+)
List values = List.of("1", "abc", "2", "xyz");
List nums = values.stream()
.map(s -> {
try { return Optional.of(Integer.parseInt(s)); }
catch (Exception e) { return Optional.empty(); }
})
.flatMap(Optional::stream)
.toList();
// [1, 2]
// Java 16+: Stream.toList()
List shorter = Stream.of(1, 2, 3).toList();
性能: Stream vs for ループ
| 条件 | 推奨 |
|---|---|
| シンプルな集計 | Stream の方が読みやすい |
| 性能シビア (μs 単位) | for ループの方が速い (Stream は生成オーバーヘッド) |
| 大量データ (10 万件以上) | parallelStream が速くなる |
| break / continue が必要 | for ループ (Stream は短絡操作のみ) |
| 外部の可変状態を更新 | for ループ (Stream は関数型で副作用回避) |
FAQ
Q: Collectors.toList() と Stream.toList() の違いは?
A: Collectors.toList() は変更可能 List。Stream.toList() (Java 16+) は不変 Listを返す (add でエラー)。
Q: parallelStream はいつでも使って良い?
A: ① 要素数が多い (1 万件以上目安)、② 各要素の処理コストが高い、③ 副作用なし — の条件で初めて有効。少量データではむしろ遅くなります。
Q: Stream で例外を処理したい
A: 関数型インタフェースは checked exception を投げられないため、内部で try-catch して RuntimeException でラップするか、Either / Try モナドを自作。または vavr ライブラリを利用。