Collectors & Grouping

The terminal operation collect gathers a stream's elements into a result, and the Collectors class provides ready-made recipes: build maps, join strings, group by a key, count, sum and partition. Together they turn many-line reporting loops into a single expression.

Collecting into collections

JAVA
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> cities = List.of("Lisbon", "Berlin", "Lisbon", "Dublin");
        Set<String> unique = cities.stream().collect(Collectors.toSet());
        TreeSet<String> sorted = cities.stream().collect(Collectors.toCollection(TreeSet::new));
        System.out.println(unique.size());
        System.out.println(sorted);
    }
}
Output
3
[Berlin, Dublin, Lisbon]

For lists, stream.toList() (Java 16+) is the shortest option and returns an unmodifiable list.

Joining strings

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

public class Main {
    public static void main(String[] args) {
        List<String> tags = List.of("java", "streams", "collectors");
        System.out.println(tags.stream().collect(Collectors.joining(", ")));
        System.out.println(tags.stream().map(t -> "#" + t).collect(Collectors.joining(" ", "Tags: ", ".")));
    }
}
Output
java, streams, collectors
Tags: #java #streams #collectors.

toMap

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

record Product(String code, String name, double price) {}

public class Main {
    public static void main(String[] args) {
        List<Product> products = List.of(new Product("P1", "Mug", 12.5), new Product("P2", "Lamp", 34.0));
        Map<String, Double> priceByCode = products.stream()
            .collect(Collectors.toMap(Product::code, Product::price));
        System.out.println(priceByCode.get("P2"));

        List<String> words = List.of("apple", "avocado", "banana");
        Map<Character, String> firstByLetter = words.stream()
            .collect(Collectors.toMap(w -> w.charAt(0), w -> w, (a, b) -> a + "+" + b, TreeMap::new));
        System.out.println(firstByLetter);
    }
}
Output
34.0
{a=apple+avocado, b=banana}

toMap(keyFn, valueFn) throws an exception on duplicate keys; the third argument says how to merge them, and the fourth chooses the map type.

groupingBy

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

record Employee(String name, String department, int salary) {}

public class Main {
    public static void main(String[] args) {
        List<Employee> staff = List.of(
            new Employee("Rafael", "Sales", 82000), new Employee("Lucas", "Sales", 52000),
            new Employee("Mei", "Engineering", 98000), new Employee("Jonas", "Engineering", 85000),
            new Employee("Amara", "Engineering", 68000), new Employee("Ethan", "Support", 60000)
        );

        Map<String, List<String>> names = staff.stream().collect(Collectors.groupingBy(
            Employee::department, TreeMap::new, Collectors.mapping(Employee::name, Collectors.toList())));
        System.out.println(names);

        Map<String, Long> headcount = staff.stream().collect(Collectors.groupingBy(
            Employee::department, TreeMap::new, Collectors.counting()));
        System.out.println(headcount);

        Map<String, Double> avgSalary = staff.stream().collect(Collectors.groupingBy(
            Employee::department, TreeMap::new, Collectors.averagingInt(Employee::salary)));
        System.out.println(avgSalary);

        Map<String, Integer> payroll = staff.stream().collect(Collectors.groupingBy(
            Employee::department, TreeMap::new, Collectors.summingInt(Employee::salary)));
        System.out.println(payroll);
    }
}
Output
{Engineering=[Mei, Jonas, Amara], Sales=[Rafael, Lucas], Support=[Ethan]}
{Engineering=3, Sales=2, Support=1}
{Engineering=83666.66666666667, Sales=67000.0, Support=60000.0}
{Engineering=251000, Sales=134000, Support=60000}

groupingBy(key) is SQL's GROUP BY for collections. A second collector — the downstream collector — decides what to compute for each group: a list, a count, an average, a sum. Passing TreeMap::new keeps the groups sorted.

partitioningBy

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

public class Main {
    public static void main(String[] args) {
        List<Integer> scores = List.of(45, 82, 67, 30, 91, 58);
        Map<Boolean, List<Integer>> passed = scores.stream()
            .collect(Collectors.partitioningBy(s -> s >= 50));
        System.out.println("Passed: " + passed.get(true));
        System.out.println("Failed: " + passed.get(false));
    }
}
Output
Passed: [82, 67, 91, 58]
Failed: [45, 30]

Finding the top item per group

JAVA
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;

record Employee(String name, String department, int salary) {}

public class Main {
    public static void main(String[] args) {
        List<Employee> staff = List.of(
            new Employee("Rafael", "Sales", 82000), new Employee("Lucas", "Sales", 52000),
            new Employee("Mei", "Engineering", 98000), new Employee("Jonas", "Engineering", 85000)
        );
        Map<String, Optional<Employee>> top = staff.stream().collect(Collectors.groupingBy(
            Employee::department, TreeMap::new, Collectors.maxBy(Comparator.comparingInt(Employee::salary))));
        top.forEach((dept, e) -> System.out.println(dept + ": " + e.map(Employee::name).orElse("-")));
    }
}
Output
Engineering: Mei
Sales: Rafael

Try it yourself

Count words by length, then list the words of each length.

Try it yourself · Main.java
Edit the code, copy it into Main.java and run java Main.java (Java 25+). How to set up Java
Output
3 letters: [sun, sky]
4 letters: [moon, star, nova]
5 letters: [comet, orbit]