reduce の使い方(集約) — 合算や累積計算
Stream の「結果をひとつに畳み込む」終端操作が reduce。合計・積・最大最小・連結・カスタム集約まで、柔軟に一行で書けます。初心者が迷う「初期値あり/なし」「並列対応」のポイントを、具体例でかみ砕いて整理します。
基本の考え方
- 役割: ストリームの全要素を「ひとつの値」に畳み込む(合算・連結・集計)。
- 形の違い:
- 初期値なし:
stream.reduce(binaryOperator)→ 結果は Optional。空ストリームで値がない可能性がある。 - 初期値あり:
stream.reduce(identity, binaryOperator)→ 常に値が返る(空なら identity がそのまま返る)。 - 並列対応3引数:
stream.reduce(identity, accumulator, combiner)→ 並列で部分結果を安全に結合。
- 初期値なし:
すぐ試せる基本例
合計(初期値あり:常に答えが出る)
import java.util.*;
import java.util.stream.*;
List<Integer> nums = List.of(1,2,3,4,5);
int sum = nums.stream()
.reduce(0, (a, b) -> a + b); // identity=0
System.out.println(sum); // 15
Java合計(初期値なし:空の可能性を考慮)
Optional<Integer> maybeSum = nums.stream()
.reduce((a, b) -> a + b);
System.out.println(maybeSum.orElse(0)); // 15(空なら0)
Java文字列連結(区切りなし)
List<String> words = List.of("Java","Stream","Reduce");
String joined = words.stream()
.reduce("", (a, b) -> a + b); // identity=""
System.out.println(joined); // JavaStreamReduce
Java例題で理解する
例題1: 最大値・最小値の取得
List<Integer> nums = List.of(7, 2, 9, 4);
// 最大
int max = nums.stream().reduce(Integer.MIN_VALUE, Math::max);
// 最小(初期値なし)
int min = nums.stream().reduce(Math::min).orElse(Integer.MAX_VALUE);
System.out.println(max); // 9
System.out.println(min); // 2
Java- ポイント: 初期値ありだと必ず値が返る。なしだと Optional で安全に扱える。
例題2: 商品金額の総計(税計算を含む)
record Item(String name, int price) {}
List<Item> items = List.of(new Item("A",100), new Item("B",250), new Item("C",160));
int totalWithTax = items.stream()
.map(i -> (int)Math.round(i.price() * 1.1)) // 税込みへ変換
.reduce(0, Integer::sum); // 合計
System.out.println(totalWithTax); // (110 + 275 + 176) の合計
Java- ポイント: reduce 前に map で「計算ルール」を適用してから畳み込む。
例題3: 文字列連結(区切りを挟む)
List<String> names = List.of("Tanaka", "Sato", "Ito");
// 先頭に余計な区切りを付けないため、初期値なしで開始
String joined = names.stream()
.reduce((a, b) -> a + ", " + b)
.orElse(""); // 空なら空文字
System.out.println(joined); // Tanaka, Sato, Ito
Java- ポイント: 区切り付き連結は「初期値なし」がスマート。
例題4: 並列計算のための3引数 reduce
import java.util.stream.*;
import java.util.*;
List<Integer> nums = IntStream.rangeClosed(1, 1_000_000).boxed().toList();
int sum = nums.parallelStream()
.reduce(0,
(partial, x) -> partial + x, // accumulator(各スレッド内)
Integer::sum // combiner(部分結果どうし)
);
System.out.println(sum);
Java- ポイント: 並列時は「分割→部分集約→結合」を定義する3引数形式が安全。
実用レシピ
- 合計(プリミティブストリームが最速)
int sum = list.stream().mapToInt(x -> x).sum(); // reduce より簡潔・高速
Java- 積(空なら1を返す)
int product = nums.stream().reduce(1, (a, b) -> a * b);
Java- フラグ集合の AND/OR 畳み込み
boolean allOk = flags.stream().reduce(true, (a, b) -> a && b);
boolean anyOk = flags.stream().reduce(false, (a, b) -> a || b);
Java- カスタム集約(合計・件数を同時に)→ まとめて平均
record Agg(long sum, long count) {}
Agg agg = nums.stream()
.reduce(new Agg(0,0),
(a, x) -> new Agg(a.sum()+x, a.count()+1),
(a, b) -> new Agg(a.sum()+b.sum(), a.count()+b.count()));
double avg = agg.count() == 0 ? 0.0 : (double) agg.sum() / agg.count();
Javaテンプレート集(そのまま使える形)
- 初期値なし(Optional で安全)
Optional<T> r = stream.reduce((a, b) -> combine(a, b));
Java- 初期値あり(必ず値が返る)
T r = stream.reduce(identity, (acc, x) -> combine(acc, x));
Java- 並列対応(3引数)
T r = stream.parallel()
.reduce(identity,
(acc, x) -> accumulate(acc, x),
(left, right) -> combine(left, right));
Java- 文字列連結(区切り付き)
String s = stream.reduce((a, b) -> a + sep + b).orElse("");
Javaよくある落とし穴と回避策
- 空ストリームで例外/不適切な初期値:
- 回避: 初期値なしなら Optional を使う。初期値ありなら意味のある identity(合計なら0、積なら1)にする。
- 非結合(associative)な演算で並列バグ:
- 回避: 並列 reduce は演算が結合的であることが前提。順序依存や外部副作用を入れない。
- 文字列連結の低速化:
- 回避: 大量連結は
Collectors.joining(", ")の方が速く簡潔。
- 回避: 大量連結は
- reduce の過剰使用:
- 回避: 合計・平均・最大最小は
mapToInt().sum(),average(),max()など専用終端を優先。
- 回避: 合計・平均・最大最小は
まとめ
- reduce は「ストリームをひとつの値に畳み込む」ための終端操作。初期値あり/なし、並列対応の3引数を使い分けると安全で強力。
- 専用メソッド(sum/average/max/joining)がある場面はそちらを優先し、reduce は「カスタム集約」に最も威力を発揮する。
- 結合的な演算・意味のある初期値・Optional の活用を押さえれば、実務でも安心して使える。
