skip to content

Explain Collectors.collectingAndThen and Collectors.teeing, and give a realistic use for each.

level: middleimportance: should knowfreq 48%

answer

  1. collectingAndThen = collect, then finisher(result) once
  2. common: toList -> unmodifiableList; maxBy -> Optional::get
  3. teeing = fork to TWO collectors, merge results, one pass
  4. teeing is Java 12+
  5. teeing uses: avg(sum,count), min+max, count+list

basics

~20 s

collectingAndThen wraps a collector and runs a final function on its result, e.g. collect into a List then make it unmodifiable. teeing (Java 12+) feeds the stream into two collectors at once and merges their two results with a combiner function, e.g. compute average as sum and count in one pass.

solid answer

~40 s

collectingAndThen(collector, finisher) adapts a collector by applying a finisher function to its final result exactly once. Classic uses: wrapping a toList()/toMap() in Collections::unmodifiableList for an immutable result, or grouping then extracting a single value. teeing(c1, c2, merger) (Java 12+) sends every element to BOTH downstream collectors simultaneously, then calls merger.apply(r1, r2) on their two results to produce one combined value — all in a single stream traversal. It's ideal when you need two aggregates of the same data, e.g. min and max together, or sum and count to compute an average yourself, or count plus a list of matches. teeing replaces the older pattern of streaming twice or wrapping in a custom collector. Both are general adapters and compose as downstreams of groupingBy.

code

java · 19 lines
java
// collectingAndThen: immutable list
List<String> names = people.stream()
    .map(Person::name)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(), Collections::unmodifiableList));

// teeing (Java 12+): average in one pass
record Stats(double avg) {}
double avg = nums.stream().collect(Collectors.teeing(
    Collectors.summingDouble(Double::doubleValue),
    Collectors.counting(),
    (sum, count) -> count == 0 ? 0.0 : sum / count));

// teeing: min and max together
record Range(int lo, int hi) {}
Range r = nums2.stream().collect(Collectors.teeing(
    Collectors.minBy(Integer::compareTo),
    Collectors.maxBy(Integer::compareTo),
    (min, max) -> new Range(min.orElseThrow(), max.orElseThrow())));

go deeper

for a junior

Knows collectingAndThen makes a result immutable and that teeing combines two collectors; may not recall versions.

for a middle

Uses collectingAndThen to wrap/extract results and teeing to compute two aggregates in one pass (Java 12+).

for a senior

Chooses teeing over double-traversal/custom collectors, handles empty/Optional results in the merger, and uses both as groupingBy downstreams.

for a principal

Weighs teeing vs. a custom Collector for complex multi-aggregate needs, considers parallel-stream behavior of the finisher/merger, and guides idiomatic usage.

## collectingAndThen — apply a finishing step `collectingAndThen(Collector<T,A,R> downstream, Function<R, RR> finisher)` runs the `downstream` collector normally, then applies `finisher` to its result **once at the end**, producing `RR`. Think of it as 'collect, *and then* transform the whole result'. ### Why it exists A `Collector` already has an internal finisher stage, but you often want to bolt on one more transformation without writing a custom collector. The most common cases: 1. **Immutability**: `collect(collectingAndThen(toList(), Collections::unmodifiableList))` → an unmodifiable `List`. (Modern Java also has `toUnmodifiableList()`, but `collectingAndThen` predates it and works for any wrapping.) 2. **Extract one value from a grouping/reduction**: e.g. group by department, take the max-paid employee, then `.get()` the `Optional`: `collectingAndThen(maxBy(comparing(Employee::salary)), Optional::get)`. 3. **Convert the container type**: collect to a `Set`, then wrap or size it. It is frequently used as a **downstream** of `groupingBy` so each group's value gets the finisher. ## teeing — fork to two collectors, then merge (Java 12+) `teeing(Collector c1, Collector c2, BiFunction merger)` passes **each element to both `c1` and `c2`**, then combines their two results via `merger.apply(result1, result2)` into a single value — in **one pass** over the stream. The name evokes a 'T' pipe splitting the flow. ### When you need it Anytime you want **two aggregates of the same elements** and a single combined answer: - **Average from scratch**: `teeing(summingDouble(x->x), counting(), (sum, n) -> n==0 ? 0 : sum/n)`. - **Min and max together**: `teeing(minBy(cmp), maxBy(cmp), (lo, hi) -> new Range(lo, hi))`. - **Count plus the matching items**: `teeing(counting(), toList(), MyResult::new)`. - **Partition-like split** computed in one traversal. ### Why one pass matters Streams are **single-use**: once consumed you can't iterate again without rebuilding the source. Before teeing you'd either stream the source twice (two passes, possible re-computation) or hand-write a custom collector that tracks both accumulators. teeing packages that for you. ## How they relate Both are *adapter collectors*. `collectingAndThen` adapts **one** collector with a post-step; `teeing` adapts **two** collectors with a merge-step. Both produce a single value and can nest as downstreams. ## Edge cases - `collectingAndThen`'s finisher runs exactly once on the combined result — fine for parallel streams. - `teeing` is `UNORDERED`-friendly but its merger must be associativity-agnostic only in the sense that it runs once on the two final results, so it is safe in parallel as long as `c1`/`c2` are well-behaved. - A `teeing` merger receiving results of empty collectors must handle them (e.g. `Optional.empty()` from `minBy`). ## Term glossary - **Finisher**: a function applied to a collector's accumulated result to produce the final value. - **BiFunction**: a function of two arguments returning one result. - **Single-use stream**: a stream can be traversed only once; teeing avoids needing two traversals.

  • How would you compute an average using teeing without averagingInt?
    teeing(summingDouble(...), counting(), (sum, n) -> n == 0 ? 0 : sum / n) — sum and count in one pass, then divide in the merger.
  • What's a clean way to return an unmodifiable grouped map value?
    Use collectingAndThen as the downstream, e.g. groupingBy(key, collectingAndThen(toList(), Collections::unmodifiableList)).

saying these in an interview costs you the question

  • Thinking teeing runs the merger per element (it runs once on the two final results)
  • Forgetting teeing requires Java 12+
  • Believing collectingAndThen can transform per-element (it transforms the whole result)
  • Using teeing mergers that don't handle empty/Optional results

context