Modern Java

44. Streams — Collectors

Collectors provides ready-made terminal collectors: groupingBy, partitioningBy, joining, toUnmodifiableList, and counting — and you can compose them for multi-level aggregation.

Java 8+ (Collectors); Java 16+ (toList()); Java 21 (core language)

Explanation

Collectors.groupingBy(classifier) groups elements into a Map<K, List<V>>. The downstream can be another collector: groupingBy(key, counting()) gives a Map<K, Long>, and groupingBy(key, groupingBy(key2)) gives nested maps.

partitioningBy(predicate) is a specialised groupingBy that always produces a Map<Boolean, List<T>> with exactly two groups. joining(delimiter, prefix, suffix) concatenates strings.

Java 16 added Stream.toList() — a convenience terminal that returns an unmodifiable List<T> (equivalent to collect(Collectors.toUnmodifiableList())). This is preferred over collect(Collectors.toList()) (which returns a mutable list) for read-only results.

Example

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class StreamsCollectors {
    record Employee(String name, String dept, double salary) {}

    public static void main(String[] args) {
        var employees = List.of(
            new Employee("Alice",   "Engineering", 95_000),
            new Employee("Bob",     "Engineering", 85_000),
            new Employee("Carol",   "Marketing",   70_000),
            new Employee("Dave",    "Marketing",   65_000),
            new Employee("Eve",     "Engineering", 105_000)
        );

        // groupingBy with downstream counting
        Map<String, Long> countByDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
        System.out.println("Count by dept: " + countByDept);

        // groupingBy with downstream averaging
        Map<String, Double> avgSalaryByDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::dept,
                     Collectors.averagingDouble(Employee::salary)));
        avgSalaryByDept.forEach((dept, avg) ->
            System.out.printf("%s: $%.0f avg%n", dept, avg));

        // partitioningBy: above/below 80k
        Map<Boolean, List<Employee>> partition = employees.stream()
            .collect(Collectors.partitioningBy(e -> e.salary() >= 80_000));
        System.out.println("Senior (>=80k): " + partition.get(true).stream().map(Employee::name).toList());
        System.out.println("Junior (<80k):  " + partition.get(false).stream().map(Employee::name).toList());

        // joining
        String names = employees.stream()
            .map(Employee::name)
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println("Names: " + names);

        // toList() (Java 16+) — unmodifiable
        List<String> engineerNames = employees.stream()
            .filter(e -> "Engineering".equals(e.dept()))
            .map(Employee::name)
            .toList(); // unmodifiable!
        System.out.println("Engineers: " + engineerNames);
    }
}

Sources