In Java Streams, downstream usually means a collector passed inside another collector.
You commonly see this in:
Collectors.groupingBy(...)Collectors.partitioningBy(...)Collectors.teeing(...)
The outer collector decides the buckets (for example, by department or by pass/fail), and the downstream collector decides what to do with each bucket.
Downstream collectors help you:
- aggregate grouped data (
counting,summingInt,averagingDouble) - transform values while grouping (
mapping) - post-process final grouped results (
collectingAndThen) - build nested summaries (for example, group then average)
Without downstream collectors, you often need extra loops or multiple passes.
var countByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::department, Collectors.counting()));Here:
- outer collector:
groupingBy(Employee::department) - downstream collector:
counting()
var namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.mapping(Employee::name, Collectors.toList())
));Here the downstream mapping(...) transforms each Employee into only a name before collecting.
var avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.averagingDouble(Employee::salary)
));counting()summingInt / summingLong / summingDoubleaveragingInt / averagingLong / averagingDoublemaxBy / minBymapping(...)filtering(...)(Java 9+)flatMapping(...)(Java 9+)collectingAndThen(...)- nested
groupingBy(...)
- Intermediate operations like
map,filter,sortedrun on the stream pipeline. - Downstream collectors run inside
collect(...), typically per group/partition.
- Prefer clear, readable collector chains over deeply nested one-liners.
- Use
toList()where appropriate in project code; useCollectors.toList()when required by APIs likemapping(...). - For
maxBy/minBy, remember values areOptional.