Skip to content
  • Collectors & groupingBy · Java

Collectors & groupingBy

LearnViaNotes @learnvianotes· 4 min read

collect(...) is the terminal operation that turns a stream into a collection, map, string or summary — and Collectors is the toolbox of recipes for it.

Into lists and sets

CallResultMutable?Nulls?
stream.toList() (16+)List<T>No — unmodifiableallowed
collect(Collectors.toList())List<T> (an ArrayList in practice)yes (not guaranteed)allowed
collect(Collectors.toUnmodifiableList()) (10+)List<T>NoNPE
collect(Collectors.toSet())Set<T> (a HashSet)yesallowed
collect(Collectors.toCollection(TreeSet::new))the exact type you chooseyesdepends
Java
import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> fixed = Stream.of("b", "a").toList();
        List<String> growable = Stream.of("b", "a").collect(Collectors.toList());
        growable.add("c");
        System.out.println(growable);
        try {
            fixed.add("c");
        } catch (UnsupportedOperationException e) {
            System.out.println("toList() result is unmodifiable");
        }
        TreeSet<String> sorted = Stream.of("pear", "apple", "fig", "apple")
                .collect(Collectors.toCollection(TreeSet::new));
        System.out.println(sorted);
    }
}
Output
[b, a, c]
toList() result is unmodifiable
[apple, fig, pear]

Into maps: toMap

Java
import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("apple", "avocado", "banana", "cherry");

        Map<String, Integer> lengths = words.stream()
                .collect(Collectors.toMap(w -> w, String::length, (a, b) -> a, TreeMap::new));
        System.out.println(lengths);

        try {
            words.stream().collect(Collectors.toMap(w -> w.charAt(0), w -> w));   // 'a' twice
        } catch (IllegalStateException e) {
            System.out.println(e.getMessage());
        }

        // merge function decides what to do with duplicate keys
        Map<Character, String> byLetter = words.stream()
                .collect(Collectors.toMap(w -> w.charAt(0), w -> w, (a, b) -> a + "|" + b, TreeMap::new));
        System.out.println(byLetter);
    }
}
Output
{apple=5, avocado=7, banana=6, cherry=6}
Duplicate key a (attempted merging values apple and avocado)
{a=apple|avocado, b=banana, c=cherry}

groupingBy — the SQL GROUP BY of Java

groupingBy(classifier) builds a Map<K, List<T>>. The two optional arguments are the map factory and a downstream collector applied to each group.

Java
import java.util.*;
import java.util.stream.*;

public class Main {
    record Emp(String name, String dept, int salary) {}

    public static void main(String[] args) {
        List<Emp> emps = List.of(
                new Emp("Asha", "Eng", 120), new Emp("Ravi", "HR", 70),
                new Emp("Zoe", "Eng", 100), new Emp("Mohan", "Sales", 80),
                new Emp("Lena", "HR", 75));

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

        Map<String, Long> counts = emps.stream().collect(
                Collectors.groupingBy(Emp::dept, TreeMap::new, Collectors.counting()));
        System.out.println(counts);

        Map<String, Integer> payroll = emps.stream().collect(
                Collectors.groupingBy(Emp::dept, TreeMap::new, Collectors.summingInt(Emp::salary)));
        System.out.println(payroll);

        Map<String, Double> avg = emps.stream().collect(
                Collectors.groupingBy(Emp::dept, TreeMap::new, Collectors.averagingInt(Emp::salary)));
        System.out.println(avg);

        Map<String, Optional<Emp>> top = emps.stream().collect(Collectors.groupingBy(
                Emp::dept, TreeMap::new, Collectors.maxBy(Comparator.comparingInt(Emp::salary))));
        top.forEach((d, e) -> System.out.println(d + " -> " + e.get().name()));
    }
}
Output
{Eng=[Asha, Zoe], HR=[Ravi, Lena], Sales=[Mohan]}
{Eng=2, HR=2, Sales=1}
{Eng=220, HR=145, Sales=80}
{Eng=110.0, HR=72.5, Sales=80.0}
Eng -> Asha
HR -> Lena
Sales -> Mohan
Downstream collectorPer group you get
counting()Long count
summingInt(f) / averagingDouble(f)sum / average
mapping(f, toList())a list of a projected field
maxBy(cmp) / minBy(cmp)Optional<T>
toSet(), joining(", ")set / concatenated string
collectingAndThen(c, fn)post-process, e.g. unwrap the Optional
groupingBy(...)nested (multi-level) grouping

partitioningBy, joining and friends

Java
import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7);

        Map<Boolean, List<Integer>> evenOdd = nums.stream()
                .collect(Collectors.partitioningBy(n -> n % 2 == 0));
        System.out.println(evenOdd);

        System.out.println(Stream.of("a", "b", "c").collect(Collectors.joining(", ", "[", "]")));

        IntSummaryStatistics st = nums.stream().collect(Collectors.summarizingInt(n -> n));
        System.out.println(st);

        Map<Integer, Long> byLength = Stream.of("hi", "yo", "hey", "java", "sun")
                .collect(Collectors.groupingBy(String::length, TreeMap::new, Collectors.counting()));
        System.out.println(byLength);
    }
}
Output
{false=[1, 3, 5, 7], true=[2, 4, 6]}
[a, b, c]
IntSummaryStatistics{count=7, sum=28, min=1, average=4.000000, max=7}
{2=2, 3=2, 4=1}

partitioningBy always has exactly two keys, false and true — even if one group is empty — which makes it nicer than groupingBy with a boolean.

Stream.toList() vs Collectors.toList()?

How would you count word frequencies with streams?

What happens with duplicate keys in Collectors.toMap?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report