reduce を一言でいうと
Stream#reduce は、
「ストリームに流れてくる複数の要素を、“1 つの値”に畳み込むための終端操作」です。
全部を 1 つに“まとめる”イメージです。
合計を出す、最大値を求める、文字列を連結する、オブジェクトに集約する――こういう処理の“本質”は全部 reduce です。
まずは一番シンプルな reduce から
Optional<T> reduce(BinaryOperator<T>) の形
一番基本のシグネチャはこれです。
Optional<T> reduce(BinaryOperator<T> accumulator)
JavaBinaryOperator<T> は、
「T と T を受け取って、T を返す関数」= (T, T) -> T です。
つまり、
「今までの結果」と「次の要素」を 1 つにまとめる関数
を渡すことで、ストリーム全体を 1 つの値に畳み込んでいきます。
例:整数の合計を reduce で書く
import java.util.List;
import java.util.Optional;
public class ReduceSumBasic {
public static void main(String[] args) {
List<Integer> nums = List.of(1, 2, 3, 4, 5);
Optional<Integer> sum =
nums.stream()
.reduce((a, b) -> a + b);
System.out.println(sum); // Optional[15]
}
}
Javaここでの reduce((a, b) -> a + b) は、
「今までの合計 a と、次の要素 b を足して、新しい合計にする」
という意味です。
ストリームが空の場合は「畳み込む元がない」ので、結果は Optional.empty() になります。
identity 付き reduce で Optional を避ける
T reduce(T identity, BinaryOperator<T> accumulator)
「空のときでも、必ず何かしらの値を返したい」場合は、
初期値(identity)を渡すオーバーロードを使います。
T reduce(T identity, BinaryOperator<T> accumulator)
Javaidentity は「畳み込みの初期値」です。
例:合計を identity 付きで書く
import java.util.List;
public class ReduceSumIdentity {
public static void main(String[] args) {
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum =
nums.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 15
}
}
Javaここでは、identity = 0、accumulator = (a, b) -> a + b です。
ストリームが空でも、
「何も足されない 0」として結果が返ってきます。
identity の意味をちゃんと理解する
identity には、
「畳み込みにおける単位元(neutral element)」
を渡すのが基本です。
足し算なら 0
掛け算なら 1
文字列連結なら “”
といった具合です。
これを守らないと、結果がズレたり、並列処理時に破綻したりします。
reduce の 3 引数版(少しだけ応用)
U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
3 引数版は、主に並列ストリームや「型を変えながら畳み込む」場面で使います。
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner)
Javaざっくり言うと、
- identity:初期値(結果の型 U)
- accumulator:
(U, T) -> U(結果と要素をどう畳み込むか) - combiner:
(U, U) -> U(部分結果同士をどうマージするか)
です。
初心者のうちは、
「型を変えたいときは collect を使う」
「3 引数 reduce は“そういうのもある”くらいで OK」
と覚えておくといいです。
よくあるパターンを reduce で書いてみる
合計・最大値・最小値
合計:
int sum = nums.stream()
.reduce(0, (a, b) -> a + b);
Java最大値:
int max = nums.stream()
.reduce(Integer.MIN_VALUE, (a, b) -> Math.max(a, b));
Java最小値:
int min = nums.stream()
.reduce(Integer.MAX_VALUE, (a, b) -> Math.min(a, b));
Javaもちろん、mapToInt().sum() や max() / min() などの専用メソッドもありますが、
「本質的には reduce で書ける」という感覚を持っておくと理解が深まります。
文字列の連結
import java.util.List;
public class ReduceJoin {
public static void main(String[] args) {
List<String> words = List.of("Java", "Stream", "reduce");
String joined =
words.stream()
.reduce("", (a, b) -> a + " " + b)
.trim();
System.out.println(joined); // "Java Stream reduce"
}
}
Javaここでは、
「今までの文字列 a に、スペースと次の単語 b をくっつける」
という畳み込みをしています。
reduce を設計するときに絶対に意識したいこと
演算は「結合的」であるべき
特に並列ストリームで reduce を使うとき、
畳み込みの演算は「結合的(associative)」である必要があります。
結合的とは、
f(f(a, b), c) == f(a, f(b, c))
Javaが成り立つことです。
足し算、掛け算、min / max などは結合的です。
一方、「順番に依存する処理」や「副作用を伴う処理」は結合的ではないことが多いです。
結合的でない演算を reduce に渡すと、
並列処理時に結果が変わったり、バグの元になります。
副作用を reduce に入れない
reduce の中で、
ログを書いたり、外部のリストに add したり、カウンタをインクリメントしたり――
といった「副作用」を書き始めると、一気にコードが壊れやすくなります。
副作用は forEach や peek に任せて、reduce は「純粋に値を畳み込む」ことだけに集中させるのが、きれいな設計です。
reduce と collect の違いをざっくり整理する
「1 つの値」か、「コンテナに集める」か
reduce と collect は、どちらも「ストリームを 1 つのものにまとめる」終端操作です。
ざっくり分けると、
- 単純な値(数値、1 つのオブジェクトなど)に畳み込みたい →
reduce - List / Set / Map などのコレクションに集めたい →
collect
という使い分けになります。
例えば、「合計」「最大値」「フラグの AND / OR」などは reduce が自然です。
一方、「リストに集める」「グルーピングする」などは collect の方が圧倒的に書きやすいです。
まとめ:reduce を自分の言葉で定義する
あなたの言葉で reduce をまとめるなら、こうなります。
「reduce は、(今までの結果, 次の要素) -> 新しい結果 という関数を使って、
ストリーム全体を 1 つの値に畳み込む終端操作」
特に意識しておきたいのは、
「合計・最大値・連結」などの“まとめる処理”は、全部 reduce で書けること
identity 付きの reduce では、“単位元”を渡すこと(足し算なら 0、掛け算なら 1 など)
演算は結合的であるべきで、副作用は入れないこと
「値 1 つ」なら reduce、「コレクションに集める」なら collect というざっくりした使い分け
あたりです。
