Definition
- Stream.reduce(identity, BinaryOperator) – terminal operation that directly produces a single value from the entire stream.
- Collectors.reducing(identity, mapper, BinaryOperator) – a Collector that can be used as a downstream inside
groupingBy,partitioningBy, and other collector compositions. - Same logic – both perform a fold (accumulate elements into a single value), but in different contexts.
- Rule – use Stream.reduce for the whole stream, Collectors.reducing for per-group aggregation.
Stream.reduce computes the bank-wide total. Collectors.reducing inside groupingBy computes the total per branch. Both do the same math, but one works on the whole ledger and the other on each branch's page.
Understanding the Concepts
What is the difference?
import java.math.BigDecimal;
import java.util.Map;
import java.util.stream.Collectors;
// Stream.reduce: whole-stream total
BigDecimal bankTotal = trades.stream()
.map(Trade::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// Collectors.reducing: per-branch total inside groupingBy
Map<String, BigDecimal> branchTotals = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));
// {Mumbai: 500000, Delhi: 300000, Chennai: 200000}
Why can you not use Stream.reduce inside groupingBy?
// WRONG: reduce is a terminal operation, not a Collector
// trades.stream().collect(groupingBy(Trade::getBranch, stream.reduce(...)));
// COMPILE ERROR!
// CORRECT: Collectors.reducing is a Collector
trades.stream().collect(Collectors.groupingBy(Trade::getBranch,
Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));
groupingBy expects a Collector as downstream. Stream.reduce is a terminal operation, not a Collector.
What are the three forms of Collectors.reducing?
import java.util.Optional;
import java.util.stream.Collectors;
// Form 1: reducing(BinaryOperator) -> Optional<T>
Map<String, Optional<BigDecimal>> maxPerBranch = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.reducing(BigDecimal::max)));
// Returns Optional because group might have no elements after mapping
// Form 2: reducing(identity, BinaryOperator) -> T
Map<String, BigDecimal> sumPerBranch = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.mapping(Trade::getAmount,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
// Form 3: reducing(identity, mapper, BinaryOperator) -> U
// Maps each element before reducing. Combines map+reduce in one collector.
Map<String, BigDecimal> sumPerBranch2 = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));
When should you use each?
| Need | Use |
|---|---|
| Single value from entire stream | Stream.reduce |
| Per-group value inside groupingBy | Collectors.reducing |
| Simple numeric sum per group | Collectors.summingInt/Long/Double |
| Custom identity/operator per group | Collectors.reducing |
| Map and reduce in one step per group | 3-arg Collectors.reducing |
How does the three-arg Collectors.reducing work?
The three-arg form reducing(identity, mapper, operator) maps each element before reducing. This combines the map and reduce steps in a single collector.
// Without three-arg: separate mapping and reducing
Map<String, BigDecimal> totals = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.mapping(Trade::getAmount,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
// With three-arg: combined in one call
Map<String, BigDecimal> totals = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));
// Cleaner: identity=ZERO, mapper=getAmount, operator=add
What about Collectors.summingInt/Long/Double?
// For simple numeric sums, summingInt/Long/Double is cleaner
Map<String, Long> countPerBranch = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch, Collectors.counting()));
Map<String, Double> sumPerBranch = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.summingDouble(Trade::getAmountDouble)));
// Use Collectors.reducing for BigDecimal or custom types
Map<String, BigDecimal> sumBD = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));
What are common mistakes?
// WRONG: trying to use Stream.reduce as a downstream
// trades.stream().collect(groupingBy(Trade::getBranch, stream.reduce(...))); // ERROR
// WRONG: using reduce for per-group aggregation outside groupingBy
// You'd need to group first, then reduce each group manually
// CORRECT: Collectors.reducing as downstream
trades.stream().collect(Collectors.groupingBy(Trade::getBranch,
Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));
What should a senior engineer know?
- Collectors.summingInt/Long/Double is syntactic sugar for simple numeric sums; prefer them for clarity.
- The three-arg Collectors.reducing(identity, mapper, operator) maps each element before reducing, combining map+reduce in one collector.
- Stream.reduce with three args (identity, accumulator, combiner) supports parallel reduction with type change.
- For BigDecimal aggregation per group, Collectors.reducing is the only built-in option since there is no Collectors.summingBigDecimal.
- Can Collectors.reducing be used standalone (not inside groupingBy)? Yes, but Stream.reduce is simpler for that case.
Visual Overview
Stream.reduce (whole stream):
Stream: [100, 200, 300] → reduce(0, add) → 600
Collectors.reducing (per group):
groupingBy(branch, reducing(0, amount, add)):
Mumbai: [100, 200] → 300
Delhi: [300] → 300
Chennai: [150, 50] → 200
Result: {Mumbai: 300, Delhi: 300, Chennai: 200}
Key Takeaway
Stream.reduce is a terminal operation for whole-stream aggregation. Collectors.reducing is a Collector for per-group aggregation inside groupingBy. Both perform the same fold logic, but in different contexts. You cannot use Stream.reduce inside groupingBy because it is a terminal operation, not a Collector.
Use Stream.reduce when you want a single value from the entire stream. Use Collectors.reducing when you need per-group reduction. For simple numeric sums, prefer Collectors.summingInt/Long/Double for clarity.
Remember one sentence: Stream.reduce aggregates the whole stream; Collectors.reducing aggregates per group inside groupingBy.
---