Stream.reduce and summing numbers
Stream.reduce overloads, identity and associativity rules, parallel reduction traps, reduce versus collect and practical ways to sum numbers in Java 21.
On this page
🧮 What is
Stream.reduce()?
reduce folds many stream elements into one result by repeatedly applying an operation, such as adding prices, finding a maximum or combining flags. It acts like a cashier adding receipts one by one: each receipt updates the running total, and at the end there is one number. For example, List.of(2, 3, 5).stream().reduce(0, Integer::sum) returns 10.
For this lesson, we'll focus on the reduction rules interviewers expect for Java 21: the three reduce overloads, true identity values, associativity, parallel combiner behavior, when collect is the better tool, and the practical ways to sum numbers. We cover primitive stream mechanics in Primitive streams: IntStream, LongStream and DoubleStream, so here we'll link there instead of repeating the whole boxing story.
Below the line (out of scope)
- Primitive ranges,
summaryStatisticsand boxing details: Primitive streams - Pipeline execution and parallel barriers: How a stream pipeline executes
- Grouped sums with downstream collectors: Grouping and partitioning
- Map value iteration patterns: Everyday Map operations
The 30-second answer
reduce combines stream elements into one value. The overloads are: reduce(identity, accumulator) for same-type reductions with a default result, reduce(accumulator) for no identity and an Optional<T>, and <U> reduce(identity, accumulator, combiner) when the result type differs or parallel combination needs a separate rule. The identity must be a true neutral value, like 0 for sum and 1 for multiplication; reduce(10, Integer::sum) is not a sum identity and parallel streams can add that 10 once per partition. The accumulator and combiner must be associative and compatible. Use reduce for immutable folds, not for mutating an ArrayList; use collect for mutable reduction. For sums, my default is mapToInt(...).sum() or mapToLong(...).sum(), Collectors.summingInt inside grouping, and BigDecimal.ZERO with BigDecimal::add for money.
What you’ll learn
- Explain the three
Stream.reduceoverloads and when each one fits - Choose a true identity and show why
reduce(10, Integer::sum)breaks in parallel - State the associativity requirement for reductions and spot non-associative accumulators
- Separate immutable reduction from mutable collection work, including why not to reduce into an
ArrayList - Compare
mapToInt().sum(),reduce(0, Integer::sum)andCollectors.summingIntfor numeric totals - Handle
long,BigDecimal, map values and overflow deliberately in Java 21
01Picture it first: receipts into one till
Picture four cashiers each adding a stack of receipts. If the store is sequential, one cashier starts at zero and adds every receipt. If the store is parallel, several cashiers each start with the identity, add their stack, and then a manager combines the subtotals.
That picture exposes the trap. The identity is used per partial reduction, not only once in the whole program. If it is not truly neutral, parallel reduction changes the answer.
A sequential reduce uses one identity and one accumulator path, while a parallel reduce uses an identity per partition and combines the partial totals
Notice that the combiner is not decoration. In parallel reduction, it is how partial answers become the final answer. In sequential execution, you may not notice a bad combiner for a long time.
02reduce under the hood: the three overloads
The simplest overload gives an identity and an accumulator. Use it when the result type is the same as the stream element type and there is a natural value for an empty stream.
Try it yourself first
Before reading on, name the identity for sum, multiplication and string concatenation. Which one would you return for an empty stream?
import java.util.List;
final class ReduceWithIdentityDemo {
public static void main(String[] args) {
List<Integer> cents = List.of(1200, 800, 500);
int total = cents.stream()
.reduce(0, Integer::sum);
int product = List.of(2, 3, 4).stream()
.reduce(1, (a, b) -> a * b);
String joined = List.of("A", "B", "C").stream()
.reduce("", (left, right) -> left + right);
System.out.println(total); // 2500
System.out.println(product); // 24
System.out.println(joined); // ABC
}
}
Notice three things:
- The identity for addition is
0because0 + x == x. - The identity for multiplication is
1because1 * x == x. - The empty string is an identity for concatenation, but repeated string concatenation in a stream is usually not the best performance choice.
The accumulator-only overload returns Optional<T> because an empty stream has no first value to start from.
import java.util.List;
import java.util.Optional;
final class ReduceOptionalDemo {
public static void main(String[] args) {
Optional<Integer> max = List.of(7, 3, 9).stream()
.reduce(Integer::max);
Optional<Integer> emptyMax = List.<Integer>of().stream()
.reduce(Integer::max);
System.out.println(max.orElseThrow()); // 9
System.out.println(emptyMax.isEmpty()); // true
}
}
This is the overload I use for max, min or "there may be no answer" cases. It avoids inventing a fake identity like Integer.MIN_VALUE when that value could be real data.
The three-argument overload is the one many candidates memorize but do not understand. It exists for a result type U that may differ from element type T, and for parallel execution where partial U results must be combined.
import java.util.List;
record Invoice(String id, int cents) { }
final class ReduceThreeArgDemo {
public static void main(String[] args) {
List<Invoice> invoices = List.of(
new Invoice("i-1", 1200),
new Invoice("i-2", 800),
new Invoice("i-3", 500)
);
int total = invoices.stream()
.reduce(
0,
(subtotal, invoice) -> subtotal + invoice.cents(),
Integer::sum);
System.out.println(total); // 2500
}
}
Here the stream element is Invoice, but the result is Integer. The accumulator turns (Integer, Invoice) into a new Integer. The combiner turns (Integer, Integer) partial totals into one total.
03Identity and associativity: where parallel bugs come from
The identity must be neutral for the operation, and the operation must be associative. Associative means regrouping does not change the answer: (a + b) + c equals a + (b + c). Addition of integers is associative in mathematical intent, though Java integer overflow still wraps; subtraction is not associative.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Advanced: reduce versus collect
- Summing numbers: the practical choices
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal