In the last part we saw,
- What is a Collector?
- How
collect()Works Internally - Commonly Used Built-in Collectors
- Grouping and Partitioning
Now we will continue and take a dive into
- Downstream Collectors (Advanced)
collectingAndThen()- Creating a custom collector
- Parallel streams and collectors
- And more
Downstream Collectors (Advanced)
Collectors can be chained.
Example: Group by department and calculate average salary.
Map<String, Double> avgSalary =
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Assuming we have the following employee list
List<Employee> employees = Arrays.asList(
new Employee("Amit", "IT", "Developer", 60000),
new Employee("Neha", "IT", "Tester", 50000),
new Employee("Raj", "HR", "Recruiter", 40000),
new Employee("Simran", "HR", "Manager", 70000),
new Employee("Karan", "Sales", "Executive", 45000)
);
Output
{
IT=55000.0,
HR=55000.0,
Sales=45000.0
}
Breakdown
- IT → (60000 + 50000) / 2 = 55000.0
- HR → (40000 + 70000) / 2 = 55000.0
- Sales → (45000) / 1 = 45000.0
mapping() Collector
Map<String, List<String>> namesByDept =
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(
Employee::getName,
Collectors.toList()
)
));
Output
{
IT=[Amit, Neha],
HR=[Raj, Simran],
Sales=[Karan]
}
How It Works
groupingBy(Employee::getDepartment) → Groups employees by department.
mapping(Employee::getName, toList()) → Instead of collecting full Employee objects, it extracts only the name.
Result type: Map<String, List<String>>
collectingAndThen()
Applies finishing transformation.
Example: Make result immutable.
List<String> names =
employees.stream()
.map(Employee::getName)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
Output
[Amit, Neha, Raj, Simran, Karan]
Creating a Custom Collector
Sometimes built-in collectors are not enough.
You can create one using:
Collector.of(
supplier,
accumulator,
combiner,
finisher
);
Example: Collect into a StringBuilder.
Collector<String, StringBuilder, String> customCollector =
Collector.of(
StringBuilder::new,
StringBuilder::append,
StringBuilder::append,
StringBuilder::toString
);
Usage:
String result =
Stream.of("A", "B", "C")
.collect(customCollector);
Output
ABC
How It Works
1 Supplier
Creates a new StringBuilder
2 Accumulator
Appends each element to the StringBuilder:
- Append "A" → "A"
- Append "B" → "AB"
- Append "C" → "ABC"
3 Combiner
Used only in parallel streams to merge partial results.
In sequential streams, it’s effectively not needed.
4 Finisher
Converts StringBuilder to String.
Parallel Streams & Collectors
Collectors must be:
- Associative
- Non-interfering
- Stateless
If you use parallel streams:
stream.parallel().collect(...)
The combiner becomes critical.
Avoid shared mutable state outside collector.
Bad code:
List<String> list = new ArrayList<>();
stream.forEach(list::add); // Not thread-safe
Good code:
stream.parallel().collect(Collectors.toList());
Performance Considerations
groupingByConcurrent() is better for parallel streams
Avoid unnecessary boxing (mapToInt() when possible)
Prefer primitive collectors (summingInt) over reduce()
Best Practices
✔ Prefer built-in collectors
✔ Use downstream collectors effectively
✔ Handle duplicate keys in toMap()
✔ Avoid side effects
✔ Use collectingAndThen() for immutability
✔ Use primitive streams when possible
When to Use collect() vs reduce()
Use:
reduce() → For immutable reduction
collect() → For mutable accumulation (most real-world cases)
Conclusion
Collectors are not just about converting streams into lists.
They allow you to:
- Transform
- Group
- Aggregate
- Partition
- Summarize
- Build custom reduction logic
Mastering Collectors means mastering the real power of Java 8 Streams.
What's next?
This concludes collectors in depth, next we will see Advanced Stream Techniques.
Top comments (0)