Advanced Collectors and Writing Custom Collectors
Fast recall
Quick revision
Core answer
toList, toSet, and toMap, Java's Collectors class offers powerful compositional operations: `groupingBy` and `partitioningBy` for classification, `counting`, `summingInt/Long/Double`, `averagingInt/Long/Double`, `summarizingInt/Long/Double` for statistical reductions, `mapping` to transform downstream, `filtering` (Java 9+) to filter downstream, `reducing` for arbitrary aggregation, and `collectingAndThen` to post-process a collector's result. A custom `Collectorsupplier() (create accumulator), accumulator() (add an element), combiner() (merge two accumulators for parallel), and finisher() (convert accumulator to result). For most use cases, `Collector.of(...)` is simpler than implementing the interface directly.Whether you know `Collectors` beyond `toList` and can compose downstream collectors for complex aggregations; whether you can describe a custom Collector's four parts
Using nested `.stream()` calls after `groupingBy` instead of composing with a downstream collector; not knowing `collectingAndThen` exists for immutable result wrapping
Explain `Collector.Characteristics` (`UNORDERED`, `CONCURRENT`, `IDENTITY_FINISH`) and when they affect parallel performance; describe a realistic custom Collector use case
Interview-ready guide
Interview answer
Beyond toList, toSet, and toMap, the power of Collectors comes from composition. The 80% use case is `groupingBy` with a downstream collector.
For example, instead of grouping into a Map and then streaming each list to sum the amounts, I do it in one pass: groupingBy(Order::getStatus, reducing(BigDecimal.ZERO, Order::getAmount, BigDecimal::add)).
The most-useful advanced collectors I reach for are `summarizingInt/Long/Double` — gives count, sum, min, max, and average in a single pass — `mapping` to transform elements into the downstream collector, `filtering` (Java 9+) to filter within groups while keeping empty groups, `collectingAndThen` to post-process (typically unmodifiableList), and `teeing` (Java 12+) to run two collectors in parallel and merge their results.
For custom collectors, Collector.of(supplier, accumulator, combiner, finisher) is the shortcut over implementing the full interface. The four parts: supplier creates the accumulator, accumulator folds one element in, combiner merges two accumulators for parallel execution, and finisher converts the accumulator to the final result. Plus characteristics — CONCURRENT, UNORDERED, IDENTITY_FINISH — which are optimization hints for parallel streams.
A practical example of writing a custom collector: variance calculation — you need to track count, sum, and sum-of-squares together. That's three metrics in one pass, which no built-in collector gives you directly.
When to write custom vs compose: I compose by default. Writing custom only makes sense for truly novel aggregations or when targeting a third-party container like a Guava ImmutableList. For most 'how do I aggregate X from a stream' problems, the answer is a nested groupingBy with the right downstream collector.
Advanced Collectors and Writing Custom Collectors
Direct Answer
Beyond toList, toSet, and toMap, Java's Collectors class offers powerful compositional operations: groupingBy and partitioningBy for classification, counting, summingInt/Long/Double, averagingInt/Long/Double, summarizingInt/Long/Double for statistical reductions, mapping to transform downstream, filtering (Java 9+) to filter downstream, reducing for arbitrary aggregation, and collectingAndThen to post-process a collector's result. A custom Collector<T, A, R> is defined by four operations: supplier() (create accumulator), accumulator() (add an element), combiner() (merge two accumulators for parallel), and finisher() (convert accumulator to result). For most use cases, Collector.of(...) is simpler than implementing the interface directly.
Common Mistakes & Pitfalls
Using nested .stream() calls after groupingBy instead of composing with a downstream collector; not knowing collectingAndThen exists for immutable result wrapping
Check your understanding
Answer these without looking back at the article.
- What's the difference between `filtering` downstream and `filter` upstream of `groupingBy`?
- When does `toMap` throw `IllegalStateException`, and how do you prevent it?
- Why is `Collector.Characteristics.CONCURRENT` not the default for all collectors?
- How would you write a custom collector that produces an immutable Guava `ImmutableMap`?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.