What is mutable reduction in the Java Streams API, and how does collect() perform it?
answer
- One container, mutated in place — not a new value per step
- supplier / accumulator / combiner
- combiner only fires in parallel
- collect = mutable, reduce = immutable
- container is reused, not re-copied
basics
~20 sMutable 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 sMutable 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
Knows collect() gathers stream elements into a List/Set/Map and can name Collectors.toList(). Understands it mutates one container.
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.
Explains the combiner's parallel role, the associativity/non-interference contract, and that an incorrect combiner silently breaks parallel results.
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)