skip to content

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

level: principalimportance: should knowfreq 38%

answer

  1. container -> collect; single value -> reduce
  2. reduce into a List is O(n^2) (copy per step)
  3. reduce combiner merges values (cheap); collect combiner merges containers (can be costly)
  4. parallel map collect -> prefer CONCURRENT collector
  5. parallel only above large n + cheap split

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).

solid answer

~60 s

reduce() is for immutable functional reduction: it folds elements with an associative, side-effect-free function into a single value, ideally where combining is O(1) and allocation-free, e.g. summing ints or finding a max. collect() is for mutable reduction: it accumulates into a mutable container that is mutated in place and merged across parallel chunks. The decision rule: if the result is a container or building it immutably would force per-element copies, use collect — reducing into a List with reduce is O(n^2) because each step must produce a new list. If the result is a small immutable value and the combine is cheap, reduce is cleaner and parallelizes well because each chunk produces a value and combining values is trivial. On parallelism: collect's combiner merges containers, whose cost depends on container size and merge complexity (e.g. merging large HashMaps is expensive — favor groupingByConcurrent); reduce's combiner merges values and is usually negligible. Parallelism only pays off above a meaningful element count and when the per-element and merge work justify the split overhead.

go deeper

for a junior

Knows collect builds collections and reduce sums/maxes; can pick the obvious one for simple cases.

for a middle

Articulates immutable vs mutable reduction and that reduce into a list is wasteful; uses collect for containers, reduce for scalars.

for a senior

Explains the O(n^2) trap, the contract differences, and that the combiner cost differs (values vs containers) under parallelism.

for a principal

Reasons quantitatively about parallel split/merge overhead, container merge complexity, when CONCURRENT collectors are required, and sets guidance on when parallel streams are worth it at all.

## Two reductions, two cost models Both `reduce` and `collect` are **terminal** stream operations that combine elements, but they target different shapes of result and have different cost profiles. ### reduce — immutable (functional) reduction `reduce` folds elements with a function `(partial, element) -> newPartial` that **returns a new value** and mutates nothing. Forms: - `reduce(BinaryOperator)` → `Optional<T>` - `reduce(identity, BinaryOperator)` → `T` - `reduce(identity, accumulator, combiner)` → `U` (the general parallel-capable form) Requirements: the operator must be **associative** and **stateless**, and `identity` must be a true identity (`op(identity, x) == x`). Ideal targets: numeric sums, products, max/min, boolean ands/ors, picking the "smallest" object — results that are a single, usually small, immutable value where combining is O(1). ### collect — mutable reduction `collect` accumulates into a **mutable container** via supplier/accumulator/combiner (+finisher). It mutates one container in place rather than producing new values. Ideal targets: `List`, `Set`, `Map`, `String` (via `joining`), grouped/partitioned structures. ## The decisive rule: is the result a container? The canonical mistake is using `reduce` to build a container: ```java // O(n^2): each step copies the whole growing list List<T> bad = stream.reduce(new ArrayList<>(), (list, x) -> { var c = new ArrayList<>(list); c.add(x); return c; }, (a, b) -> { var c = new ArrayList<>(a); c.addAll(b); return c; }); ``` To keep `reduce` truly immutable you must copy the container each step → **O(n^2)** time and n allocations. (Mutating the seed in place instead would *work sequentially* but is **unsafe in parallel** and violates `reduce`'s contract — the seed is shared across chunks.) `collect` exists precisely to do this right: one container, in-place mutation, merge only across chunks → **O(n)**. So: **container result → `collect`; single immutable value with cheap combine → `reduce`.** ## Parallel performance considerations Going parallel splits the source, processes chunks on the common ForkJoinPool, and merges. The merge cost differs sharply: - **reduce**: chunks produce values; the combiner merges *values* (e.g. add two longs) → merge cost ~O(1) per merge, negligible. reduce parallelizes beautifully for cheap associative ops. - **collect**: chunks produce *containers*; the combiner merges *containers*. Merging two `ArrayList`s is O(size); merging two `HashMap`s with `groupingBy` can be expensive and even re-bucket. For map-heavy parallel work, a **CONCURRENT** collector (`groupingByConcurrent`, `toConcurrentMap`) avoids merging by sharing one concurrent container — often the only way parallel collection beats sequential. General parallelism caveats apply to both: there must be **enough elements** (rough rule of thumb: tens of thousands+ with non-trivial per-element work) to amortize split/merge/pool overhead; the source must **split cheaply** (arrays/ArrayList good; LinkedList/IO-backed poor); and the pipeline must be **stateless and non-interfering**. Below the threshold, sequential wins. ## Decision checklist 1. Result is a **container/aggregate structure**? → `collect`. 2. Result is a **single immutable value** and combine is **cheap & associative**? → `reduce`. 3. Going **parallel**? For `collect` into a map, prefer a **CONCURRENT** collector to dodge costly merges; for `reduce`, ensure a real identity and associativity. 4. Don't parallelize unless the input is large, splits cheaply, and the work per element justifies it — otherwise sequential is faster and simpler. ## One-liner heuristics - Building a List/Set/Map/String → `collect`. - Summing/maxing/folding to one value → `reduce`. - `reduce` to build a collection → almost always a bug.

  • Why is mutating the reduce() identity/seed in place a bug even though it seems to work?
    reduce's contract requires a stateless, side-effect-free accumulator and a true identity. In parallel, the single seed is shared across chunks, so in-place mutation causes data races and wrong results. It may pass sequentially, masking the defect. Use collect for mutable accumulation.
  • For a parallel groupingBy over millions of elements, what changes the performance the most?
    Switching to groupingByConcurrent (a CONCURRENT collector) so threads share one ConcurrentHashMap and skip the expensive merging of many partial HashMaps that plain groupingBy requires.

saying these in an interview costs you the question

  • Using reduce to accumulate into a List/Map (O(n^2) or unsafe seed mutation)
  • Assuming collect always parallelizes well — merging large maps can dominate
  • Mutating a shared reduce seed in place to fake mutable reduction (breaks parallel correctness)
  • Going parallel for small inputs or poorly-splitting sources expecting a speedup

context