SJ blog
Java
Z

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

Java Stream APIを完全活用する — filter/map/collectから並列処理まで

Java 8で導入されたStream APIの基本操作から、collectors、flatMap、並列ストリームまで実例で解説します。

一言結論

Stream APIはループを書く代わりにfilter/map/collectで処理を宣言的に記述でき、並列化も.parallelStream()一行で試せるが、小さいコレクションではオーバーヘッドが上回るため計測して使うこと。

Stream API とは

コレクションのデータを宣言的に処理するAPIです。ループを書かずに変換・フィルタ・集計が書けます。

// 従来のforループ
List<String> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.toUpperCase());
    }
}

// Stream API
List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

Streamの作成

// コレクションから
List<String> list = List.of("a", "b", "c");
Stream<String> stream = list.stream();

// 配列から
String[] array = {"x", "y", "z"};
Stream<String> stream = Arrays.stream(array);

// 直接指定
Stream<String> stream = Stream.of("a", "b", "c");

// 範囲(IntStream)
IntStream.range(0, 10)  // 0〜9
IntStream.rangeClosed(1, 10)  // 1〜10

中間操作(Intermediate Operations)

中間操作は 遅延評価 されます(終端操作が呼ばれるまで実行されない)。

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// filter: 条件に合うものだけ残す
numbers.stream().filter(n -> n % 2 == 0)  // [2, 4, 6, 8, 10]

// map: 各要素を変換
numbers.stream().map(n -> n * n)  // [1, 4, 9, 16, 25, ...]

// sorted: ソート
numbers.stream().sorted(Comparator.reverseOrder())

// distinct: 重複除去
Stream.of(1, 2, 2, 3, 3).distinct()  // [1, 2, 3]

// limit / skip
numbers.stream().skip(3).limit(5)  // [4, 5, 6, 7, 8]

// peek: デバッグ用(値を確認しながら流す)
numbers.stream()
    .filter(n -> n > 5)
    .peek(n -> System.out.println("filtered: " + n))
    .map(n -> n * 2)
    .collect(Collectors.toList());

flatMap: ネストを平坦化

List<List<Integer>> nested = List.of(
    List.of(1, 2, 3),
    List.of(4, 5),
    List.of(6, 7, 8, 9)
);

List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

終端操作(Terminal Operations)

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

// collect: リストやマップに変換
List<String> list = names.stream().collect(Collectors.toList());

// count
long count = names.stream().filter(n -> n.length() > 3).count();

// findFirst / findAny
Optional<String> first = names.stream()
    .filter(n -> n.startsWith("C"))
    .findFirst();

// anyMatch / allMatch / noneMatch
boolean anyLong = names.stream().anyMatch(n -> n.length() > 5);
boolean allShort = names.stream().allMatch(n -> n.length() < 10);

// reduce: 畳み込み
int sum = IntStream.rangeClosed(1, 10).reduce(0, Integer::sum);

Collectors の活用

List<Person> people = ...;

// グルーピング
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::getCity));

// カウント
Map<String, Long> countByCity = people.stream()
    .collect(Collectors.groupingBy(Person::getCity, Collectors.counting()));

// 文字列結合
String joined = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));
// [Alice, Bob, Charlie, Dave]

// 統計
IntSummaryStatistics stats = people.stream()
    .mapToInt(Person::getAge)
    .summaryStatistics();
System.out.println("平均: " + stats.getAverage());
System.out.println("最大: " + stats.getMax());

並列ストリーム

// parallelStream() で並列処理
long count = names.parallelStream()
    .filter(n -> n.length() > 3)
    .count();

注意: 並列ストリームは常に速いわけではありません。

  • 要素数が少ない場合はオーバーヘッドで逆に遅い
  • 順序に依存する処理は注意が必要
  • スレッドセーフでない操作(ArrayList への追加など)はNG

よくある間違い

// NG: Streamは一度しか使えない
Stream<String> stream = names.stream();
stream.filter(...).collect(...);
stream.map(...);  // IllegalStateException: stream has already been operated upon or closed

// NG: forEach で外部変数を変更する
List<String> result = new ArrayList<>();
names.stream().forEach(n -> result.add(n));  // 並列時に危険

// OK: collect を使う
List<String> result = names.stream().collect(Collectors.toList());

まとめ

操作メソッド説明
中間filter条件で絞り込む
中間map各要素を変換
中間flatMapネストを平坦化
中間sortedソート
終端collectコレクションに変換
終端reduce畳み込み
終端count/sum集計

Stream API はコードの意図を明確に表現できます。まず filter + map + collect の組み合わせから慣れていきましょう。