この記事を読むべき理由
Stream.reduceは、試験でも実務でも頻出のメソッドですが、「初期値あり」と「初期値なし」の2種類があることに気づかず、戻り値の型の違いで引っかかるケースが多いです。
「なんとなく使えるけど、試験で問われると自信がない」という方に向けて、型・動作・ひっかけポイントまで整理します。
reduceの基本:何をするメソッドか
reduceはストリームの要素を一つの値に集約するための終端操作です。
たとえば整数のリストがあったとき、全要素の合計を求めるのが代表的な使い方です。ループで書けば自明な処理ですが、reduceを使うことでストリームのパイプラインに組み込めます。
初期値ありのreduce:戻り値はT
import java.util.List;
public class ReduceWithIdentity {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 15
}
}シグネチャはこうなっています。
T reduce(T identity, BinaryOperator<T> accumulator)identity(初期値)を渡すと、戻り値はそのままT型になります。Optionalではないため、get()の呼び出しは不要です。
空のストリームの場合
List<Integer> empty = List.of();
int result = empty.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(result); // 0ストリームが空でも、初期値である0がそのまま返ります。例外は発生しません。
初期値なしのreduce:戻り値はOptional<T>
import java.util.List;
import java.util.Optional;
public class ReduceWithoutIdentity {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
sum.ifPresent(System.out::println); // 15
}
}シグネチャはこうです。
Optional<T> reduce(BinaryOperator<T> accumulator)初期値がないと、ストリームが空だったときに返す値がありません。そのためOptional<T>を返す設計になっています。
空のストリームの場合
List<Integer> empty = List.of();
Optional<Integer> result = empty.stream()
.reduce((a, b) -> a + b);
System.out.println(result.isPresent()); // falseOptional.empty()が返ります。ここでresult.get()を呼ぶとNoSuchElementExceptionが発生します。
2つのreduceを比較する
| 項目 | 初期値あり | 初期値なし |
|---|---|---|
| シグネチャ | T reduce(T identity, BinaryOperator<T> acc) | Optional<T> reduce(BinaryOperator<T> acc) |
| 戻り値の型 | T | Optional<T> |
| 空ストリーム時 | 初期値を返す | Optional.empty()を返す |
| NPEリスク | accumulatorがnullを返すと発生 | Optional経由なので直接的なNPEは起きにくい |
| 典型的な用途 | 合計・積など「0やからの空値が自然な場合」 | 最大値・最小値など「空に意味がある場合」 |
3引数のreduce:parallelStreamで使う形
あまり注目されませんが、試験に登場することがあります。
import java.util.List;
public class ReduceThreeArgs {
public static void main(String[] args) {
List<String> words = List.of("Java", "Gold", "Study");
int totalLength = words.parallelStream()
.reduce(
0,
(sum, s) -> sum + s.length(), // accumulator
(a, b) -> a + b // combiner
);
System.out.println(totalLength); // 14
}
}シグネチャはこうです。
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)3引数版の特徴は、型を変換しながら集約できる点です。ここではStringのストリームをintに畳み込んでいます。
combinerは並列処理で分割された部分結果を最終的にまとめる役割を持ちます。シーケンシャルストリームではcombinerが呼ばれないことがほとんどです。
試験で狙われるひっかけポイント
ひっかけ1:初期値ありなのにOptionalで受け取ろうとする
// コンパイルエラー
Optional<Integer> result = numbers.stream()
.reduce(0, (a, b) -> a + b);reduce(T identity, BinaryOperator<T>)の戻り値はT(ここではInteger)です。Optional<Integer>では受け取れません。
ひっかけ2:初期値なしなのにintで受け取ろうとする
// コンパイルエラー
int result = numbers.stream()
.reduce((a, b) -> a + b);戻り値はOptional<Integer>なので、intには代入できません。
ひっかけ3:identityは「単位元」でなければならない
identityは数学的な単位元(演算を適用しても値が変わらない要素)を渡す前提で設計されています。
| 演算 | 正しいidentity |
|---|---|
| 加算 | 0 |
| 乗算 | 1 |
| 文字列結合 | "" |
たとえば乗算でidentityに0を渡すと、常に0が返ります。試験では「意図的に間違った初期値」を選択肢に混ぜてくることがあります。
ひっかけ4:3引数のcombinerがシーケンシャルでは無視される
List<String> words = List.of("Java", "Gold");
int result = words.stream() // parallelStreamではない
.reduce(
0,
(sum, s) -> sum + s.length(),
(a, b) -> { throw new RuntimeException("呼ばれた"); } // 例外は発生しない
);
System.out.println(result); // 8シーケンシャルストリームではcombinerは実行されません。parallelStreamとの違いをセットで覚えておきましょう。
まとめ
Stream.reduceは、初期値の有無によって戻り値の型が変わるという、Javaの型システムをうまく活用した設計になっています。
| 覚えること | ポイント |
|---|---|
初期値あり → T | Optional不要、空でも初期値が返る |
初期値なし → Optional<T> | 空ストリーム対応のためOptionalで包まれる |
3引数版 → U | 型変換可能、combinerは並列時のみ動作する |
試験本番では「戻り値の型は何か」「空ストリームのときどうなるか」の2点を軸に選択肢を絞り込むと正答率が上がります。reduceの仕組みをしっかり理解した上で、実際にコードを動かして確認しておくことをおすすめします。