Advanced Collectors
Combining collectors like Lego blocks — collectingAndThen, mapping, filtering, flatMapping, reducing, teeing and nested collector pipelines.
#Introduction
This is where most Java developers stop… and where senior Java developers begin.
By now you know filter(), map(), sorted(), distinct(), reduce(), groupingBy() and partitioningBy(). In this chapter you'll learn how to combine collectors to solve complex problems in one Stream pipeline: collectingAndThen(), mapping(), filtering(), flatMapping(), reducing(), teeing() and nested collectors.
#What Are Advanced Collectors?
Think of collectors like Lego blocks. You already know Collectors.toList(), Collectors.counting() and Collectors.summingInt(). Now imagine combining them:
That's exactly what advanced collectors do.
#collectingAndThen()
Probably the most asked advanced collector.
Collectors.collectingAndThen(
downstreamCollector,
finisher
)Example: Sorted Names
Without collectingAndThen() — two steps:
List<String> names = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());
Collections.sort(names);With collectingAndThen() — everything happens in one collector:
List<String> names = employees.stream()
.collect(Collectors.collectingAndThen(
Collectors.mapping(Employee::getName, Collectors.toList()),
list -> {
Collections.sort(list);
return list;
}
));Make a List Immutable
Very common in production.
List<String> names = employees.stream()
.map(Employee::getName)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));Result: an unmodifiable list.
#mapping()
You've already seen it inside groupingBy(). Without it you get Map<String, List<Employee>>; with it you get Map<String, List<String>>.
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));IT -> [John, Alex]
HR -> [David]#filtering() (Java 9+)
Instead of filtering before collecting:
employees.stream()
.filter(e -> e.getSalary() > 50000)
.collect(...);you can filter inside grouping:
Map<String, List<Employee>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.filtering(
e -> e.getSalary() > 50000,
Collectors.toList()
)
));This keeps all departments in the result map, even if some departments end up with empty lists.
Why filtering()?
Suppose IT has John (70000) and Alex (30000), and HR has Bob (25000).
Sometimes that's exactly what reporting requires.
#flatMapping() (Java 9+)
class Employee {
String name;
String department;
List<String> skills;
}Data: John → Java, SQL; Alex → Java, Docker. Requirement: department → all skills.
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.flatMapping(
e -> e.getSkills().stream(),
Collectors.toSet()
)
));IT -> [Java, SQL, Docker]Without flatMapping(), you'd end up with nested lists (List<List<String>>).
#reducing()
Most people know reduce(). Very few know Collectors.reducing().
Map<String, Integer> totalAge = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.reducing(0, Employee::getAge, Integer::sum)
));IT 120
HR 75The three arguments are identity, mapper and combining operator.
#teeing() (Java 12+)
One of the coolest collectors: it runs two collectors in one traversal and merges their results.
Need: count and average. Normally that's count() and average() — two traversals.
record Stats(long count, double avg) {}
Stats stats = employees.stream()
.collect(Collectors.teeing(
Collectors.counting(),
Collectors.averagingDouble(Employee::getSalary),
Stats::new
));Stats[count=15, avg=65000.0]One traversal.
#Nested Collectors
Requirement: department → name of the highest-paid employee.
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
opt -> opt.map(Employee::getName).orElse("")
)
));This is a senior-level Stream pipeline.
#Real Spring Boot Examples
// Department -> immutable employee list
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
)
));
// Department -> employee names
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
// Department -> high-salary employees
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.filtering(e -> e.getSalary() > 50000, Collectors.toList())
));
// Department -> unique skills
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.flatMapping(e -> e.getSkills().stream(), Collectors.toSet())
));#Interview Questions
Q1Difference between map() and mapping()?
| map() | mapping() |
|---|---|
| Stream operation | Collector |
Used before collect() | Used inside another collector |
| Returns a Stream | Used within groupingBy() etc. |
Q2Difference between filter() and filtering()?
| filter() | filtering() |
|---|---|
| Stream operation | Collector |
| Before grouping — empty groups disappear | During grouping — empty groups are kept |
Q3When do we use collectingAndThen()?
When we want to transform the collected result — e.g. make it immutable, sort it, or convert Optional<T> → value.
Q4Difference between reduce() and Collectors.reducing()?
reduce() is a terminal operation on the stream: stream.reduce(...). reducing() is a collector, typically used as a downstream collector:
Collectors.groupingBy(
...,
Collectors.reducing(...)
)Q5What does teeing() do?
Runs two collectors over the same elements in one pass and merges the results with a function.
#Practice Problems
class Employee {
int id;
String name;
String department;
List<String> skills;
double salary;
}- Group employees by department and return only names.
- Group by department and return immutable employee lists.
- Group by department and keep only employees with salary > 60,000.
- Group by department and return unique skills.
- Group by department and return the total salary using
reducing(). - Group by department and return the highest-paid employee.
- Group by department and return only the highest-paid employee's name.
- Count employees and average salary in one pass using
teeing(). - Group by department and return sorted employee names.
- Group by department and return unmodifiable sets of employee names.
5+ Years Interview Challenge
Return a Map<String, List<String>> where key = department and value = sorted, unique employee names, salary > 50,000 only.
Map<String, List<String>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.filtering(
e -> e.getSalary() > 50000,
Collectors.mapping(
Employee::getName,
Collectors.toCollection(TreeSet::new)
)
),
ArrayList::new
)
));Can we make it truly immutable?
The previous solution returns a mutable ArrayList. To make the final lists immutable:
Map<String, List<String>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.filtering(
e -> e.getSalary() > 50_000,
Collectors.mapping(
Employee::getName,
Collectors.toCollection(TreeSet::new)
)
),
names -> List.copyOf(names)
)
));Now each department maps to an unmodifiable, sorted, unique list of names.
#Pro Tips
1. Know where each operation belongs
map(),filter(),flatMap()→ Stream operations.mapping(),filtering(),flatMapping()→ Collector operations.
This distinction is a common interview question.
2. Use collectingAndThen() to finish cleanly
Ideal for making collections immutable, converting Optional<T> to T, and sorting or transforming a collected result before returning it.
3. Don't overcomplicate pipelines
Advanced collectors are powerful, but readability matters. If a pipeline becomes difficult to understand, splitting it into two clear steps is often the better engineering choice.
#Chapter Summary
collectingAndThen()— collect, then finishmapping(),filtering(),flatMapping()as downstream collectorsfilter()vsfiltering()— empty groupsCollectors.reducing()teeing()— two collectors, one pass- Nested collector pipelines