- Local Variable Type Inference in Java
- Sealed Classes and Interfaces In Java
- Records In Java
- Java Stream API: What It Is (and What It Is Not)
- Creating and Consuming Streams in Java
- Stream Operations and Pipelines Explained
- Aggregating Stream Data Using the reduce Operation
- Bi-Argument Functional Interfaces in the Stream API
Introduction
Among all stream operations, reduce is both the most powerful and the most misunderstood. It looks simple, yet it encodes strict mathematical and semantic rules that, if violated, can silently produce incorrect results—especially when streams run in parallel.
This article is entirely dedicated to understanding reduce correctly: its purpose, its variants, its constraints, and its proper use cases. Mastering reduce is a decisive step toward writing robust, predictable, and parallel-safe stream code.
“
reduceis not about looping; it is about associative aggregation.”
1. What Does reduce Really Do?
At a conceptual level, reduce aggregates a stream of elements into a single result by repeatedly combining two values into one.
This combination:
- must be associative,
- must not depend on element order (unless order is guaranteed),
- must be side-effect free.
In functional terms, reduce collapses a sequence into a scalar.
2. The Simplest Form: reduce(BinaryOperator<T>)
The most minimal form of reduce takes a single argument: a BinaryOperator<T>.
Example: Compute the sum of a list of numbers
List<Integer> numbers = List.of(1, 2, 3, 4);
int sum = numbers.stream()
.reduce((a, b) -> a + b)
.orElse(0);
System.out.println("Simple reduce sum: " + sum);// Simple reduce sum: 10
Here:
ais the accumulated value,bis the next stream element.
Because the stream might be empty, the result is wrapped in an Optional.
“No identity means no guaranteed result.”
3. Reduce With an Identity Value
To avoid dealing with Optional, you can provide an identity value.
List<Integer> numbers = List.of(1, 2, 3, 4);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Reduce with identity sum: " + sum);// Reduce with identity sum: 10
The identity must satisfy the rule:
identity ⊕ x = x
for all values x.
Examples of valid identities:
0for addition1for multiplication""for string concatenation
Choosing an incorrect identity leads to incorrect results.
4. Associativity: The Golden Rule of reduce
Associativity is not optional. It is the core contract of reduce.
By definition, an operation ⊕ is associative if:
(a ⊕ b) ⊕ c = a ⊕ (b ⊕ c)
Addition is associative. Subtraction is not.
// Dangerous: subtraction is not associative
numbers.stream().reduce((a, b) -> a - b);
“If your operation is not associative,
reduceis the wrong tool.”
5. The Three-Argument reduce and Parallel Streams
The most general form of reduce includes:
- an identity,
- an accumulator,
- a combiner.
int sum = numbers.parallelStream()
.reduce(
0,
(a, b) -> a + b, // accumulator
(x, y) -> x + y // combiner
);
In this example, the stream of numbers is processed in parallel. Each parallel substream starts from the identity value (0) and uses the accumulator to sum its elements locally. Once all substreams have produced partial sums, the combiner merges those partial results into a single final sum.
The combiner exists because:
- parallel streams split data,
- each chunk is reduced independently,
- partial results must be merged.
Accumulator and combiner must:
- be associative,
- be compatible with each other,
- produce the same logical result.
6. Why reduce and Mutation Do Not Mix
A frequent anti-pattern is using reduce to mutate a container.
// Do NOT do this
list.stream().reduce(
new ArrayList<>(),
(acc, e) -> { acc.add(e); return acc; }
);
This breaks:
- immutability expectations,
- parallel safety,
- semantic clarity.
“If you mutate state,
reduceis no longer a reduction.”
Use collect instead for mutable aggregation.
7. Reduce vs Collect: A Clear Boundary
| Goal | Correct Operation |
|---|---|
| Combine values immutably | reduce |
| Build a collection | collect |
| Aggregate with mutation | collect |
| Parallel-safe scalar result | reduce |
Remember:
“
reduceproduces a value;collectproduces a container.”
8. When reduce Should Be Avoided
Even when valid, reduce is not always the best choice.
Avoid reduce when:
- a built-in terminal operation exists (
sum,max,count) - readability suffers
- mutation is required
Streams favor clarity over cleverness.
Conclusion
The reduce operation is a cornerstone of functional-style programming in Java streams. Used correctly, it enables elegant, parallel-safe aggregation. Used incorrectly, it introduces subtle and dangerous bugs.
By respecting associativity, identity rules, and immutability, developers can harness the full power of reduce with confidence.
“Master
reduce, and parallel streams stop being a mystery.”
You can find the complete code of this article here on GitHub.
