What does reduce() operation do
The reduce() operation (also known as a functional fold) is a terminal operation in the Java Stream API that sequentially aggregates all elements of a stream into a single conso...
🟢 Junior Level
30-Second Summary
The reduce() operation (also known as a functional fold) is a terminal operation in the Java Stream API that sequentially aggregates all elements of a stream into a single consolidated scalar value using an associative accumulation function (such as computing a sum, finding a product, calculating min/max, or string concatenation).
Unlike collect(), reduce() implements immutable reduction: on each step, the accumulator generates a brand-new value rather than mutating an existing container in place.
There are three overloaded variants of reduce():
reduce(BinaryOperator<T> accumulator): ReturnsOptional<T>(safely handling empty streams).reduce(T identity, BinaryOperator<T> accumulator): ReturnsT(returns the providedidentityif the stream is empty).reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner): Allows the accumulated result typeUto differ from stream element typeT.
Java 21 Code Examples
import java.util.List;
import java.util.Optional;
public class ReduceBasicsDemo {
public static void main(String[] args) {
List<Integer> numbers = List.of(2, 3, 4, 5);
// 1. Without identity: returns Optional<T>
Optional<Integer> productOpt = numbers.stream()
.reduce((a, b) -> a * b);
System.out.println("Product: " + productOpt.orElse(0)); // 120
// 2. With identity: returns T directly (identity = 0 for addition)
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum); // 14
// 3. Finding maximum value
int max = numbers.stream()
.reduce(Integer.MIN_VALUE, Integer::max);
System.out.println("Maximum: " + max); // 5
// 4. Behavior on an empty stream
List<Integer> empty = List.of();
int emptyResult = empty.stream().reduce(42, Integer::sum);
System.out.println("Empty stream with identity: " + emptyResult); // 42
}
}
🟡 Middle Level
The Monoid Mathematical Contract: Identity and Associativity
For reduce() to execute deterministically across both sequential and parallel streams, the accumulator must satisfy the mathematical properties of a Monoid:
1. The Neutral Element (identity)
The identity value must act as the algebraic identity for the accumulator function:
\(\text{accumulator}(\text{identity}, v) == v\)
- For addition: $\text{identity} = 0$ ($0 + x = x$).
- For multiplication: $\text{identity} = 1$ ($1 \times x = x$).
- For finding max: $\text{identity} = -\infty$ (
Integer.MIN_VALUE). - For string concatenation: $\text{identity} = “”$ ($”” + s = s$).
[!CAUTION] The Non-Neutral Identity Hazard: If you provide
identity = 100when computing a sum, a sequential stream adds100once. But in aparallelStream()with 4 worker threads, each worker thread seeds its own chunk with100, inflating the final total by $4 \times 100 = 400$!
2. Associativity
The grouping of operations must not alter the final result:
\((a \oplus b) \oplus c == a \oplus (b \oplus c)\)
Associativity guarantees that ForkJoinPool worker threads can split the dataset at arbitrary points and merge sub-results in any hierarchical tree order:
Sequential Evaluation: ((1 + 2) + 3) + 4 = 10
Parallel Tree Evaluation: (1 + 2) + (3 + 4) = 3 + 7 = 10
[!WARNING] Non-Associative Trap: Operations like subtraction (
a - b) or division are non-associative: $(10 - 2) - 3 = 5$, but $10 - (2 - 3) = 11$. Running.parallelStream().reduce((a, b) -> a - b)produces chaotic, non-deterministic results!
🔴 Senior Level
The 3-Argument Overload: Why the Combiner Is Needed
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
This overload is essential when transforming elements of type T into an aggregate scalar of a different type U:
List<String> words = List.of("Java", "Stream", "API");
int totalLength = words.parallelStream()
.reduce(
0, // identity: U (Integer)
(acc, str) -> acc + str.length(), // accumulator: (U, T) -> U
Integer::sum // combiner: (U, U) -> U
);
Execution Mechanics:
- In Sequential Streams: The
combineris never executed. The single calling thread iterates through elements, invoking onlyaccumulator.apply(u, t). - In Parallel Streams:
- Each worker thread in
ForkJoinPoolprocesses a local slice of elementsT, folding them into a partial aggregate of typeUusing theaccumulator. - As worker tasks finish,
ForkJoinPoolinvokescombiner.apply(u1, u2)to hierarchically merge the partial results of typeUinto a single final value.
- Each worker thread in
Anti-Pattern: Mutable Reduction via reduce() ($O(N^2)$ Complexity)
A frequent interview trap is attempting to collect elements into a collection using reduce():
// ❌ CRITICAL ANTI-PATTERN: O(N^2) allocations and concurrency bugs!
List<String> badList = stream.reduce(
new ArrayList<>(),
(list, item) -> {
list.add(item); // Mutates shared identity or creates a new list copy!
return list;
},
(left, right) -> {
left.addAll(right);
return left;
}
);
Why This Is Broken:
- Parallel Data Races: In a parallel stream, all worker threads receive the identical instance of
identity = new ArrayList<>(). Concurrent calls tolist.add()corrupt the internal array structure. - Algorithmic Explosion: If you avoid mutation by creating a new
ArrayListon each step, copying $i$ elements on step $i$ produces an arithmetic progression $\sum_{i=1}^N i = O(N^2)$ allocations and CPU cycles. - The Proper Tool: For accumulating into mutable containers (
List,Set,Map), always usecollect()(mutable reduction).
4 Tricky Questions
1. Why does using reduce() to accumulate elements into an ArrayList cause $O(N^2)$ algorithmic complexity or concurrency data races?
Answer:
reduce() is designed for functional immutable folding. If you adhere to immutability and construct a new ArrayList at each step, appending element $i$ requires copying all previous $i-1$ elements into a new array. Summed over $N$ items, the total operations equal $\frac{N(N+1)}{2}$, resulting in disastrous $O(N^2)$ time complexity and immense garbage collector pressure.
Conversely, if you attempt to mutate the list in place within the lambda, passing new ArrayList<>() as the identity means all parallel worker threads share the exact same list instance. Concurrently invoking list.add() triggers race conditions, corrupted element counts, or ArrayIndexOutOfBoundsException. For mutable accumulation, the Stream API provides collect(), which allocates thread-isolated intermediate containers.
2. What happens in a parallelStream() if you pass an incorrect non-neutral identity value (e.g., 10 when summing numbers)?
Answer:
In a sequential stream, the initial identity value of 10 is incorporated once at the start of the iteration, resulting in 10 + totalSum.
However, in a parallelStream(), the dataset is split into $K$ independent sub-tasks executed across ForkJoinPool worker threads. Each worker task seeds its private partial accumulator with the identity value (10). When sub-tasks finish and their results are combined, the initial 10 is added $K$ times. The final sum will be inflated by $10 \times K$, producing a completely invalid and non-deterministic result that fluctuates based on CPU core availability.
3. Why is the combiner function never executed when running the 3-argument reduce() on a sequential stream?
Answer:
In sequential execution, there is only one thread processing the stream pipeline. The single accumulator value of type U is initialized once with identity and updated in place element by element via accumulator.apply(u, t) until the end of the stream.
Because there are no parallel worker threads producing separate partial results, there are never two instances of type U that need to be merged. The stream implementation in ReferencePipeline detects single-threaded mode and bypasses the combiner entirely as an optimization.
4. How do the algebraic properties of a Monoid guarantee correct execution of reduce() across multi-core processors?
Answer: A Monoid consists of an algebraic set equipped with an associative binary operation and an identity element:
- Neutral Identity: Guarantees that any number of worker threads can instantiate a local partial accumulator without distorting the final calculation.
- Associativity: Guarantees that $(a \oplus b) \oplus (c \oplus d) == ((a \oplus b) \oplus c) \oplus d$. This mathematical invariance allows
ForkJoinPoolto partition data into arbitrary binary tree leaves and combine intermediate results as worker threads complete, without requiring any specific evaluation order.
🎯 Interview Cheat Sheet
Three reduce() Overloads Comparison Matrix
| Signature | Return Type | Empty Stream Result | Combiner Role |
| :— | :— | :— | :— |
| reduce(accumulator) | Optional<T> | Optional.empty() | N/A (homogeneous type $T$) |
| reduce(identity, accumulator) | T | Returns identity | N/A (homogeneous type $T$) |
| reduce(identity, accumulator, combiner) | U | Returns identity | Merges partial $U$ results in parallelStream |
Key Takeaways
- Immutable Folding:
reduce()folds stream elements into a single scalar value without mutating objects. - Monoid Laws: Must satisfy Neutral Identity (
identity + x = x) and Associativity ((a+b)+c = a+(b+c)). - Never for Containers: Do not use
reduce()to build lists or sets; usecollect()to avoid $O(N^2)$ copy overhead. - Primitive Streams: Use
mapToInt().sum()instead ofreduce(0, Integer::sum)to avoid millions of heap auto-boxing allocations.
Red Flags & Anti-Patterns to Avoid
- ❌ Using non-associative operations (subtraction, division) in
reduce(): Fails completely in parallel streams. - ❌ Using
reduce()to accumulate intoArrayListorStringBuilder: Causes $O(N^2)$ copying or multi-threaded race conditions. - ❌ Supplying a non-neutral
identityvalue: In parallel streams, the identity is multiplied by the number of worker chunks. - ❌ Assuming the
combinerruns in sequential streams: It is invoked exclusively in parallel streams.