skip to content

collect & Mutable Reduction

collect is mutable reduction: a container is supplied, filled by an accumulator, and merged by a combiner, which is what Collector packages up. Collector characteristics explain how the work is shared and merged in parallel.

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

questions

5

What is mutable reduction in the Java Streams API, and how does collect() perform it?

level: juniorimportance: must knowfreq 70%

answer

  1. One container, mutated in place — not a new value per step
  2. supplier / accumulator / combiner
  3. combiner only fires in parallel
  4. collect = mutable, reduce = immutable
  5. container is reused, not re-copied

basics

~20 s

Mutable reduction combines stream elements by adding them into one mutable container (like a List or StringBuilder) instead of creating a new value each step. collect() does this: it makes a container, then drops each element into it.

solid answer

~40 s

Mutable reduction is a reduction that accumulates stream elements into a single mutable result container — a List, Map, StringBuilder, etc. — rather than producing a new immutable value at each step like reduce() does. The collect() terminal operation performs it. In its three-argument form, collect(supplier, accumulator, combiner): the supplier creates a fresh empty container, the accumulator folds each element into that container (a side-effecting BiConsumer), and the combiner merges two partial containers into one (used when the stream runs in parallel). The more common one-argument form, collect(Collector), packages those same three functions (plus a finisher) into a reusable Collector object, e.g. Collectors.toList(). Mutable reduction is preferred over reduce() whenever the result is a container, because repeatedly allocating new immutable containers per element would be quadratic and wasteful.

go deeper

for a junior

Knows collect() gathers stream elements into a List/Set/Map and can name Collectors.toList(). Understands it mutates one container.

for a middle

Can articulate the supplier/accumulator/combiner triple and write the three-arg collect form; knows reduce is immutable and collect is mutable, and why a container result favors collect.

for a senior

Explains the combiner's parallel role, the associativity/non-interference contract, and that an incorrect combiner silently breaks parallel results.

for a principal

Frames mutable reduction in terms of allocation cost and parallel split/merge; can reason about when a custom Collector or three-arg collect is justified versus reaching for a library Collector.

## The problem mutable reduction solves A **stream** in Java is a pipeline that processes a sequence of elements (e.g. `list.stream()`). A **terminal operation** ends the pipeline and produces a result. **Reduction** means combining all the elements into a single result. There are two flavors of reduction: 1. **Immutable (functional) reduction** — `reduce()`. Each step takes the running result and the next element and returns a *brand-new* result value, never modifying anything. Great for `int` sums, `max`, string-of-numbers, etc., where the result is small and cheap to copy. 2. **Mutable reduction** — `collect()`. Instead of producing a new result each step, you keep **one mutable container** and *mutate it in place*, adding each element to it. Why does mutable reduction exist? Consider building a `List` of a million elements with `reduce`. Each step would have to create a new list containing all previous elements plus one more — copying the whole list every time. That is O(n²) and allocates a million lists. Mutable reduction instead creates **one** `ArrayList` and calls `add()` a million times — O(n), one container. ## The three (well, four) functions `collect` is defined by three pieces of behavior: - **supplier** — `Supplier<R>`: creates a new, empty result container. Example: `ArrayList::new`. - **accumulator** — `BiConsumer<R, T>`: folds one element `T` into the container `R` by **side effect** (it returns nothing; it mutates). Example: `List::add`. - **combiner** — `BiConsumer<R, R>`: merges the contents of a second partial container into the first. Example: `List::addAll`. This is only used when the stream is split for **parallel** execution — each worker thread builds its own partial container, and the combiner stitches them together. The low-level three-arg form exposes these directly: ```java List<String> result = stream.collect( ArrayList::new, // supplier ArrayList::add, // accumulator ArrayList::addAll // combiner ); ``` A **Collector** (the one-arg form `collect(Collector)`) bundles those three functions *plus* a fourth, the **finisher** (`Function<A,R>`), which transforms the mutable accumulation type `A` into the final result type `R` (often identity — no transform). `Collectors.toList()`, `Collectors.toMap(...)`, `Collectors.joining()` are all pre-built Collectors. ## Sequential vs parallel - **Sequential:** the supplier is called once, the accumulator runs for every element, the combiner is *never* called. - **Parallel:** the stream is split into chunks; each chunk gets its own container from the supplier and is accumulated independently, then partial containers are merged pairwise by the combiner. The container can be **reused/merged** rather than recreated, which is the whole performance point. ## Correctness contract For `collect` to give a correct, parallel-safe answer, the supplier/accumulator/combiner must form an **associative** and **non-interfering** reduction: the combiner of two accumulations must equal accumulating both into one. If `accumulator` and `combiner` disagree (e.g. one sorts and the other doesn't), parallel results become wrong or nondeterministic. ## Bottom line Use `collect` (mutable reduction) when your result is a *container*; use `reduce` (immutable reduction) when your result is a single *value* and combining is cheap and side-effect-free.

  • Why is building a List with reduce() a bad idea compared to collect()?
    reduce() must produce a new value each step, so building a List means copying the whole accumulated list per element — O(n^2) time and one allocation per element. collect() keeps a single mutable ArrayList and calls add(), which is O(n).
  • What is the fourth function a Collector adds beyond the three-arg collect?
    A finisher: Function<A,R> that transforms the intermediate mutable accumulation type A into the final result type R. It is often the identity transform (IDENTITY_FINISH).

saying these in an interview costs you the question

  • Saying collect() returns a new container for each element (that is reduce, and it would be O(n^2))
  • Claiming the combiner always runs (it only runs for parallel streams)
  • Confusing collect (mutable reduction) with reduce (immutable functional reduction)

context

open as a page

When would you use the three-argument collect(supplier, accumulator, combiner) instead of collect(Collector), and how do the two forms relate?

level: middleimportance: should knowfreq 55%

basics

~20 s

The three-arg collect lets you spell out the container, how to add an element, and how to merge two containers, all inline. collect(Collector) is the packaged version using ready-made collectors like Collectors.toList(). Prefer the packaged ones; use three-arg for quick one-off containers.

open as a page

What are the three Collector characteristics (CONCURRENT, UNORDERED, IDENTITY_FINISH), and how does each affect parallel collection?

level: seniorimportance: should knowfreq 45%

basics

~20 s

They are hints a Collector gives the stream. CONCURRENT means all threads can safely share one container. UNORDERED means element order doesn't matter so the stream can skip preserving it. IDENTITY_FINISH means the accumulation container is already the final result, so the finisher can be skipped.

open as a page

What contract must the supplier, accumulator, and combiner of a collect() satisfy for a correct parallel result, and what goes wrong if it is violated?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The combiner must produce the same result as accumulating everything into one container. The functions must not interfere with the source or share state badly. If they disagree, a parallel stream gives wrong or random results, even though the sequential version looks fine.

open as a page

When should you choose collect() over reduce() (and vice versa), and how does parallel performance factor in?

level: principalimportance: should knowfreq 38%

basics

~20 s

Use reduce() when combining gives a single immutable value (a sum, max, the smallest object) and combining is cheap. Use collect() when the result is a mutable container (List, Map, String). collect avoids copying a growing container per element, so it scales; reduce on a container would be O(n^2).

open as a page