こんにちは、かつコーチです。filterやmapで加工したStreamを、結局どうやって最終結果として取り出せばいいのか迷った経験はありませんか。
今回はStream処理の締めくくりとなる終端操作(Streamを消費して最終的な結果を返す操作)の代表格、collectとreduceを解説します。
終端操作とは?
終端操作の役割
終端操作はStreamパイプラインの最後に1回だけ呼び出す操作で、この呼び出しをきっかけにこれまでの中間操作がまとめて実行されます。
終端操作を実行するとStreamは消費され、以降そのStreamは再利用できません。
collectとreduceの違い
| 観点 | collect | reduce |
|---|---|---|
| 主な用途 | List・Set・Mapなどのコレクションに詰め直す | 全要素を1つの値に集約する |
| 典型例 | Collectors.toList()、Collectors.groupingBy() | 合計・最大値・文字列連結 |
| イメージ | 「容器に集める」 | 「畳み込んで1つにする」 |
基本の書き方
collectでListやMapに詰め直す
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
record Product(String name, int price) {}
List<Product> products = List.of(
new Product("キーボード", 8000),
new Product("マウス", 3000),
new Product("モニター", 25000)
);
List<String> names = products.stream()
.map(Product::name)
.collect(Collectors.toList());
Map<String, Integer> priceMap = products.stream()
.collect(Collectors.toMap(Product::name, Product::price));
System.out.println(names); // [キーボード, マウス, モニター]
System.out.println(priceMap); // {キーボード=8000, マウス=3000, モニター=25000}
Collectors.toList()はListに、Collectors.toMap(キー抽出, 値抽出)はMapに詰め直します。
次回の記事でCollectorsの便利なメソッドをさらに詳しく解説します。
reduceで1つの値に集約する
import java.util.List;
import java.util.Optional;
List<Integer> prices = List.of(8000, 3000, 25000);
int total = prices.stream()
.reduce(0, (a, b) -> a + b);
Optional<Integer> max = prices.stream()
.reduce((a, b) -> a > b ? a : b);
System.out.println(total); // 36000
System.out.println(max.get()); // 25000
reduce(初期値, 演算)は初期値を起点に要素を1つずつ演算しながら畳み込みます。
初期値を省略したreduce(演算)は要素が空の可能性があるため、戻り値がOptionalになります。
よくあるつまずきポイント・エラー対処
toMapでキーが重複してエラーになった
実際に私が業務データで遭遇したのが、Collectors.toMapでキーが重複して実行時エラーになったケースです。
// ❌Before:同じ名前の商品が2件あるデータでtoMapする
List<Product> products = List.of(
new Product("キーボード", 8000),
new Product("キーボード", 9500) // 名前が重複
);
Map<String, Integer> priceMap = products.stream()
.collect(Collectors.toMap(Product::name, Product::price));
このコードは java.lang.IllegalStateException: Duplicate key キーボード になります。toMapはキーの重複を検知すると、デフォルトでは例外を投げる仕様になっているためです。
// ✅After:重複時のマージ方法を第3引数で指定する
Map<String, Integer> priceMap = products.stream()
.collect(Collectors.toMap(
Product::name,
Product::price,
(existing, replacement) -> existing // 既存の値を優先
));
System.out.println(priceMap); // {キーボード=8000}
toMapの第3引数にマージ関数を渡すことで、「重複したら後の値で上書きする」「既存の値を優先する」といったルールを明示できます。
実データを扱う際は重複の可能性を常に疑い、マージ関数まで含めて設計するのが安全です。
応用・一歩先の使い方
joiningで文字列を連結する
String joined = products.stream()
.map(Product::name)
.collect(Collectors.joining("、", "商品一覧: ", "です"));
System.out.println(joined); // 商品一覧: キーボード、マウス、モニターです
Collectors.joining(区切り, 接頭辞, 接尾辞)を使うと、StringBuilderを自分で組み立てなくても整形済みの文字列を作れます。
reduceで文字列を組み立てる
String csv = List.of("キーボード", "マウス", "モニター").stream()
.reduce((a, b) -> a + "," + b)
.orElse("");
System.out.println(csv); // キーボード,マウス,モニター
単純な連結であればjoiningの方が効率的ですが、独自の集約ロジックが必要な場合はreduceが使えることも覚えておくと応用が利きます。
まとめ
この記事のポイント
- 終端操作はStreamパイプラインの最後に1回だけ呼び、この呼び出しで中間操作がまとめて実行される
collectはコレクションへの詰め直し、reduceは1つの値への集約に使うCollectors.toMapはキーが重複すると例外になるため、第3引数でマージ方法を指定する- 文字列連結は
Collectors.joining、独自の集約ロジックはreduceが向いている
次に読むべき記事
- 中間操作(filter・map・sorted)
- Collectorsの活用
タグ: Java, 中級者向け, 関数型プログラミング