skip to content

reduce & Reduction

The three reduce forms and the laws they depend on: a genuine identity, an associative accumulator, and a combiner consistent with it. Interviewers ask why the three-argument form exists, and the answer is parallel reduction.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What are the three overloaded forms of Stream.reduce, and what does each return?

level: juniorimportance: must knowfreq 70%

answer

  1. 1 arg = Optional (no seed)
  2. 2 args = identity + accumulator, plain value
  3. 3 args = + combiner, type can change
  4. combiner exists for parallel merge
  5. empty stream: form 1 empty Optional, forms 2/3 return identity

basics

~20 s

reduce folds a stream into one result. The one-arg form (just a combine function) returns an Optional because the stream may be empty. The two-arg form takes a starting value plus a combine function and returns a plain value. The three-arg form adds a combiner used when running in parallel.

solid answer

~40 s

Stream.reduce has three overloads. (1) reduce(BinaryOperator) takes only an accumulator and returns Optional<T> — Optional because an empty stream has no element to return, and there is no seed to fall back on. (2) reduce(identity, BinaryOperator) takes a starting identity value plus an accumulator and returns a plain T; on an empty stream it returns the identity. (3) reduce(identity, BiFunction accumulator, BinaryOperator combiner) returns U, allowing the result type to differ from the element type, and supplies a combiner that merges partial results — essential for parallel streams where the work is split, reduced per chunk, then combined. Choose the form by whether you need an Optional, a seeded value, or a type-changing/parallel reduction.

code

java · 16 lines
java
List<Integer> nums = List.of(2, 3, 4);

// Form 1: Optional, no seed
Optional<Integer> sum1 = nums.stream().reduce((a, b) -> a + b); // Optional[9]
Optional<Integer> empty = List.<Integer>of().stream().reduce(Integer::sum); // Optional.empty

// Form 2: identity + accumulator -> plain value
int sum2 = nums.stream().reduce(0, Integer::sum); // 9 (0 if empty)

// Form 3: result type U differs from element type T, plus combiner
List<String> words = List.of("ab", "cde");
int totalLen = words.stream()
    .reduce(0,
            (len, w) -> len + w.length(), // accumulator: U,T -> U
            Integer::sum);                // combiner:   U,U -> U
// totalLen == 5

go deeper

for a junior

Can state that reduce folds a stream into one result and name the three forms by argument count, knowing the one-arg form returns Optional.

for a middle

Explains why form 1 returns Optional, what identity does on an empty stream, and that the three-arg form lets the result type differ from the element type.

for a senior

Articulates the role of the combiner for parallel merging, when each form is the right choice, and the type signatures (BinaryOperator vs BiFunction).

for a principal

Frames reduce within the broader reduction/fold abstraction, relates it to collect and to the associativity/identity laws, and reasons about parallel-decomposition correctness and performance trade-offs.

## What "reduce" means A **stream** in Java is a lazy sequence of elements you process with a pipeline of operations. A **reduction** (also called a *fold*) takes that whole sequence and collapses it into a single result by repeatedly combining elements — for example summing numbers, finding a max, or concatenating strings. `Stream.reduce` is the general-purpose reduction operation; specialized ones like `sum()`, `count()`, `max()` are reductions too. ## The mental model Think of reduce as a loop with an accumulator variable: ``` result = identity for (element : stream) result = accumulator(result, element) return result ``` The **accumulator** is a function of two arguments — the running result so far and the next element — that produces the new running result. The **identity** is the starting value of `result`. ## The three overloads **1. `Optional<T> reduce(BinaryOperator<T> accumulator)`** — one argument. A `BinaryOperator<T>` is a function taking two `T`s and returning a `T` (e.g. `(a, b) -> a + b`). There is **no identity** here, so if the stream is empty there is nothing to return — hence the result is wrapped in `Optional<T>`, which is either present (a value) or empty. The first element seeds the reduction; each later element is folded in. **2. `T reduce(T identity, BinaryOperator<T> accumulator)`** — two arguments. You supply the **identity** (the seed). Because there is always at least the identity, the result is a plain `T`, never an Optional. On an empty stream it returns the identity unchanged. The accumulator still maps `(T, T) -> T`, so the result type equals the element type. **3. `U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)`** — three arguments. Here the result type `U` may **differ** from the element type `T`. The accumulator is now a `BiFunction<U, T, U>`: it folds a `T` element into a `U` result. Because partial results are of type `U`, a separate **combiner** `BinaryOperator<U>` is needed to merge two partial `U` results into one. This matters for **parallel** streams: the framework splits the data, reduces each chunk to a partial `U`, and then merges the partials with the combiner. In a sequential stream the combiner is rarely invoked, but it must still be correct and consistent with the accumulator. ## Why an Optional only for form 1 Forms 2 and 3 always have an identity to return, so they can never be "empty"; form 1 has no seed, so emptiness is a real possibility and `Optional` expresses it honestly instead of returning null. ## Quick examples - Form 1: `stream.reduce((a, b) -> a + b)` → `Optional<Integer>`. - Form 2: `stream.reduce(0, Integer::sum)` → `int` (0 if empty). - Form 3: `words.reduce(0, (len, w) -> len + w.length(), Integer::sum)` → total length, with a combiner that adds partial lengths.

  • Why does only the one-argument reduce return an Optional?
    Because it has no identity/seed value. On an empty stream there is no element and no fallback to return, so emptiness is a genuine outcome and Optional models it instead of returning null.
  • In a sequential stream, is the combiner from the three-arg form ever called?
    Rarely or never for a straightforward sequential reduction — partial results aren't split — but it must still be supplied and be correct, because the same code may run in parallel and JDK behavior shouldn't be relied on to skip it.

Reduce is like stacking coins into one pile. Form 1: you start with the first coin found (and have nothing if the box is empty). Form 2: you start the pile at zero, so even an empty box gives you a pile of zero. Form 3: workers each build a sub-pile in parallel, then a combiner merges the sub-piles into the final stack.

saying these in an interview costs you the question

  • Saying the one-arg form returns a plain value (it returns Optional).
  • Claiming the combiner only matters in the two-arg form (the two-arg form has no combiner).
  • Thinking the three-arg form's accumulator is a BinaryOperator (it's a BiFunction U,T -> U).
  • Assuming reduce always needs three arguments to work in parallel (two-arg can parallelize too when T==U).

context

open as a page

What correctness laws must identity, accumulator, and combiner satisfy for reduce to give correct (and parallel-safe) results?

level: middleimportance: must knowfreq 60%

basics

~20 s

The identity must be a neutral value: combining it with any element leaves the element unchanged (like 0 for addition). The accumulator must be associative, so the order of grouping doesn't change the answer. And the combiner must give the same result as the accumulator. Break these and parallel results become wrong or nondeterministic.

open as a page

How does reduce behave on an empty stream across its three forms, and how should you handle the Optional result?

level: juniorimportance: should knowfreq 45%

basics

~20 s

On an empty stream the one-arg reduce returns an empty Optional, while the two-arg and three-arg forms return the identity you gave. For the Optional, don't call get() blindly — use orElse, orElseGet, ifPresent, or map so an empty result is handled safely.

open as a page

When should you use reduce versus collect for a reduction, and why?

level: seniorimportance: should knowfreq 65%

basics

~20 s

Use reduce when you combine values into a new immutable result, like summing numbers or finding a max. Use collect when you accumulate into a mutable container, like building a List, Map, or StringBuilder. reduce makes a fresh value each step; collect mutates one container, which is far more efficient for large results.

open as a page

How does reduce decompose a parallel stream, and what makes a reduction a good or bad candidate for parallelization?

level: principalimportance: should knowfreq 35%

basics

~20 s

In a parallel stream, reduce splits the data into chunks, reduces each chunk to a partial result, then merges the partials with the combiner. It pays off only when the data is large, splitting is cheap, the accumulator and combiner are fast and law-abiding, and there's no shared mutable state or boxing overhead.

open as a page