DEV Community

Priyank Bhardwaj
Priyank Bhardwaj

Posted on

A Complete Guide to Collectors in Java 8 Streams - Part 2

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)
             ));
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

Output

{
  IT=55000.0,
  HR=55000.0,
  Sales=45000.0
}
Enter fullscreen mode Exit fullscreen mode

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()
                 )
             ));
Enter fullscreen mode Exit fullscreen mode

Output

{
  IT=[Amit, Neha],
  HR=[Raj, Simran],
  Sales=[Karan]
}
Enter fullscreen mode Exit fullscreen mode

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
             ));
Enter fullscreen mode Exit fullscreen mode

Output

[Amit, Neha, Raj, Simran, Karan]
Enter fullscreen mode Exit fullscreen mode

Creating a Custom Collector

Sometimes built-in collectors are not enough.

You can create one using:

Collector.of(
    supplier,
    accumulator,
    combiner,
    finisher
);
Enter fullscreen mode Exit fullscreen mode

Example: Collect into a StringBuilder.

Collector<String, StringBuilder, String> customCollector =
    Collector.of(
        StringBuilder::new,
        StringBuilder::append,
        StringBuilder::append,
        StringBuilder::toString
    );
Enter fullscreen mode Exit fullscreen mode

Usage:

String result =
    Stream.of("A", "B", "C")
          .collect(customCollector);
Enter fullscreen mode Exit fullscreen mode

Output

ABC
Enter fullscreen mode Exit fullscreen mode

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(...)
Enter fullscreen mode Exit fullscreen mode

The combiner becomes critical.

Avoid shared mutable state outside collector.

Bad code:

List<String> list = new ArrayList<>();
stream.forEach(list::add); // Not thread-safe
Enter fullscreen mode Exit fullscreen mode

Good code:

stream.parallel().collect(Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

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)