Detailed explanation
Stream.reduce() combines stream elements into one result using an accumulation operation. The main difference between its two common overloads is how the initial result and an empty stream are handled. reduce(T identity, BinaryOperator<T> accumulator) returns T directly, using the identity when there are no elements. reduce(BinaryOperator<T> accumulator) returns Optional<T>, because an empty stream has no element from which to produce a result. For example, Stream.of(1, 2, 3).reduce(0, Integer::sum) returns 6, while Stream.of(1, 2, 3).reduce(Integer::max) returns Optional[3]. Both are terminal operations that consume the stream.
An identity is not an arbitrary default or a starting offset: it must be neutral for the operation. The API requires accumulator.apply(identity, t) to equal t for every relevant t. Therefore, 0 is an identity for addition, 1 for multiplication, and the empty string for string concatenation. Conceptually, a sequential reduction with an identity starts with that value and folds each element into the running result. Without an identity, the first element becomes the initial result, and accumulation proceeds over the remaining elements. A one-element stream therefore needs no accumulator invocation in the identity-free overload.
The identity-free form is useful when no natural neutral value exists in the domain, or when absence itself matters. For instance, finding the maximum of arbitrary values should not use 0 as a supposed identity: a stream containing only negative integers would incorrectly return 0. Optional makes the empty case explicit; callers can handle it with orElse, orElseThrow, or ifPresent rather than assuming a result exists. For a maximum specifically, Stream.max(comparator) often communicates intent more clearly.
Both overloads require an associative accumulator that is stateless and non-interfering. This matters particularly in parallel execution, where partial reductions are computed independently and combined. The identity may participate in multiple partial reductions, so a non-neutral seed can produce incorrect results. Subtraction is unsuitable because regrouping changes its result. Interviewers usually look beyond the return types: explain empty-stream behaviour, choose a valid identity, and connect associativity and neutrality to parallel correctness.
Key points to remember
- reduce(identity, accumulator) returns T and yields the identity for an empty stream.
- reduce(accumulator) returns Optional<T> and yields Optional.empty() for an empty stream.
- An identity must be neutral for the operation, not merely a convenient fallback.
- Without an identity, a single-element stream returns that element wrapped in Optional.
- Both overloads require associative, stateless, non-interfering accumulation for correct reduction.
Common mistakes candidates make
- Using a non-neutral identity, such as 0 for a maximum over potentially negative integers.
- Calling Optional.get() without accounting for an empty stream.
- Using subtraction or a stateful accumulator and assuming parallel reduction will preserve sequential behaviour.
Likely follow-up questions
Why can a non-neutral identity give different sequential and parallel results?
Parallel reduction may apply the identity separately to multiple partitions before combining their results. For addition, using 10 instead of 0 can therefore add that offset multiple times rather than once.
What does the three-argument reduce() overload do?
reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner) allows the result type U to differ from the element type T. The accumulator incorporates elements into results, while the combiner merges partial results; both must obey the API's compatibility and associativity requirements.
Should reduce() be used to build a mutable collection?
Prefer collect() for mutable accumulation, such as building an ArrayList. Its supplier creates separate containers for partial computations, avoiding the shared mutable identity problems that can arise with reduce().
Interview tip: Start with the return types and empty-stream behaviour, then use addition and a negative-number maximum to explain why a valid identity matters.
