Writing a Collector
Four functions, and the combiner that only runs in parallel — so a broken one passes every sequential test.
The built-in collectors cover most needs — toList, groupingBy, joining, teeing. When they do not, you write one, and the four functions it needs are easy. One of them is only ever called in parallel, which is why a broken collector can pass every test you write.
The four functions
A Collector is a recipe for folding a stream into a result:
| Function | Job | When it runs |
|---|---|---|
| supplier | make an empty container | once per chunk |
| accumulator | add one element to a container | once per element |
| combiner | merge two containers into one | only in parallel |
| finisher | turn the container into the result | once, at the end (optional) |
Collector.of takes them as arguments. Here is one that finds the minimum and maximum in a single pass — something the built-ins can do only by traversing twice:
static final class MinMax {
int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
void accept(int x) { if (x < min) min = x; if (x > max) max = x; }
MinMax combine(MinMax o) { min = Math.min(min, o.min); max = Math.max(max, o.max); return this; }
}
Collector<Integer, MinMax, MinMax> minMax =
Collector.of(MinMax::new, MinMax::accept, MinMax::combine);### sequential : min=1 max=1000000
### parallel : min=1 max=1000000Correct both ways. Now the version worth studying.
The combiner that passes every sequential test
Change one thing — a combiner that keeps its left side and throws away the right:
Collector.of(MinMax::new, MinMax::accept, (left, right) -> left);### sequential : min=1 max=1000000 <- still right
### parallel : min=1 max=31250 <- wrongSequentially it is still correct. Parallel, the maximum is 31,250 instead of a million.
The reason is the table above: a sequential stream never calls the combiner. It uses one container for everything, so the combiner can be completely wrong and nothing notices. A parallel stream splits the source into chunks — the streams lesson's trySplit — gives each its own container, and merges them with the combiner. Discard the right side of every merge and you keep only the first chunk's result, which is exactly what 31250 is.
So a broken collector ships, passes review, passes every test, and produces a wrong answer the day somebody changes stream() to parallelStream() for performance.
Characteristics, and the one that lies
A collector may declare characteristics that let the stream skip work:
IDENTITY_FINISH— the container is the result, so the finisher is skipped.UNORDERED— encounter order does not matter, which frees a parallel stream to merge in any order.CONCURRENT— the accumulator is safe to call from many threads on one shared container, so no combining is needed.
The dangerous one is CONCURRENT. Declaring it on a collector whose container is not thread-safe is not a performance hint — it is a data race, because the stream will call your accumulator from several threads at once. The torn-read measurement in the pure-functions lesson is what that produces. Only declare it for a genuinely concurrent container such as a ConcurrentHashMap.
When to write one at all
Write a collector when:
- You want one pass over something the built-ins would traverse twice.
- The result is a domain object built from many elements — a histogram, a summary, a report line.
- You need it reusable across many streams, where repeating a
reducewould be noise.
And not when a built-in composes to the same thing. teeing combines two collectors into one result, and groupingBy with a downstream collector covers a surprising amount:
.collect(teeing(minBy(naturalOrder()), maxBy(naturalOrder()), (lo, hi) -> ...))That is MinMax in one line, from built-ins, with a combiner somebody else tested. Prefer it — the collector above is worth writing when the fold is genuinely yours, not when the library already has it.
reduce is the lighter alternative for a single value, and it has the same trap in a different place: its combiner argument must be consistent with its accumulator, and it too runs only in parallel.