3.

Java Stream API 完全ガイド

編集
この記事の要点
  • Stream API は Java 8 で導入されたコレクション操作の関数型 API
  • パイプライン: collection.stream().filter(...).map(...).collect(Collectors.toList())
  • 中間操作 (Intermediate): filter / map / sorted / distinct → 遅延評価
  • 終端操作 (Terminal): collect / forEach / count / reduce → ここで初めて実行
  • Stream は再利用不可 (1 回 consume したら閉じる) / parallelStream で並列化可

Stream API の基本

Java 8 で導入された java.util.stream は、コレクション (List / Set / Map) や配列に対して関数型スタイルで一連の処理を記述するための API です。for ループより簡潔に書け、並列化も容易です。

import java.util.*;
import java.util.stream.*;

List<String> names = List.of("Alice", "Bob", "Carol", "Dave");

// 例1: 大文字化して List に
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// [ALICE, BOB, CAROL, DAVE]

// 例2: 3 文字以下のみ
List<String> short_ = names.stream()
    .filter(n -> n.length() <= 3)
    .collect(Collectors.toList());
// [Bob]

// 例3: チェイン (filter + map + collect)
List<String> result = names.stream()
    .filter(n -> n.startsWith("A") || n.startsWith("C"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// [ALICE, CAROL]

パイプラインの構造

Stream の処理は「Source → 中間操作 → 終端操作」の 3 段構成です。

段階役割
SourceStream の生成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<Integer> 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<List<Integer>> 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<Integer> nums = List.of(1, 2, 3, 4, 5);

// collect: 集約
List<Integer> list = nums.stream().collect(Collectors.toList());
Set<Integer> 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<Integer> 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<Integer> sumOpt = nums.stream().reduce(Integer::sum);         // Optional[15]

// findFirst / findAny
Optional<Integer> 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<Person> 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<String, List<Person>> byDept = people.stream()
    .collect(groupingBy(Person::dept));
// {Sales=[Alice, Carol], Tech=[Bob, Dave]}

// groupingBy + counting
Map<String, Long> countByDept = people.stream()
    .collect(groupingBy(Person::dept, counting()));
// {Sales=2, Tech=2}

// groupingBy + averagingInt
Map<String, Double> avgAge = people.stream()
    .collect(groupingBy(Person::dept, averagingInt(Person::age)));
// {Sales=26.5, Tech=32.5}

// partitioningBy: 二分割
Map<Boolean, List<Person>> over30 = people.stream()
    .collect(partitioningBy(p -> p.age() >= 30));
// {false=[Alice, Carol], true=[Bob, Dave]}

// toMap
Map<String, Integer> 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<Integer> boxed = IntStream.range(0, 5).boxed();
// 逆変換
int[] arr = Stream.of(1, 2, 3).mapToInt(Integer::intValue).toArray();

遅延評価

Stream は終端操作が呼ばれるまで何も実行しません。中間操作を 100 個チェインしても、終端操作がなければゼロ動作です。

List<String> 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<Integer> 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<Integer> result = new ArrayList<>();
nums.parallelStream().forEach(result::add);   // ❌ ArrayList はスレッドセーフでない

// ✅ 集約は Collectors を使う
List<Integer> safe = nums.parallelStream().collect(Collectors.toList());

Stream は再利用不可

Stream<String> 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<Stream<String>> 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<String> values = List.of("1", "abc", "2", "xyz");
List<Integer> nums = values.stream()
    .map(s -> {
        try { return Optional.of(Integer.parseInt(s)); }
        catch (Exception e) { return Optional.<Integer>empty(); }
    })
    .flatMap(Optional::stream)
    .toList();
// [1, 2]

// Java 16+: Stream.toList()
List<Integer> 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 ライブラリを利用。

編集
Post Share
子ページ
  1. InputStream
  2. OutputStream
同階層のページ
  1. 文字列API
  2. 日時API
  3. ストリームAPI
  4. サーブレットAPI

最近更新/作成されたページ