All topics
streams
intermediate

Collectors

Build results from a stream — list, set, map, grouping, summary stats — using ready-made collector factories.

A collector tells a stream what to build at the end.

A mailroom sorter — by ZIP code, then by street, then count letters per address. Each level uses a smaller sorter underneath.

Key Concepts

1
    .collect(toList())            builds a List
    .collect(groupingBy(fn))      builds a Map of groups
    .collect(joining(", "))       builds one String
2
Collectors is the class that holds the ready-made ones. You will use five or six of them constantly, and two of those throw exceptions in situations people do not expect.
Collectors
3
groupingBy is the one you reach for most. Give it a function that says what to group by:
groupingBy
4
    groupingBy(User::getDept)
    // Map<Dept, List<User>>   -- every user, filed under their department
5
You can then say what to build for each group instead of a list:
6
    groupingBy(User::getDept, counting())
    // Map<Dept, Long>         -- how many people per department
7
    groupingBy(User::getDept, mapping(User::getName, toList()))
    // Map<Dept, List<String>> -- just the names
8
That second argument is another collector, which is why they combine so freely.
9
partitioningBy splits in two. It takes a yes/no test and always returns both keys, even when one side is empty:
partitioningBy
10
    partitioningBy(u -> u.getAge() >= 18)
    // Map<Boolean, List<User>>, with both true and false present
11
That guarantee is why it beats groupingBy when your grouping is a simple test — no null checks on the empty side.
groupingBy
12
toMap throws on a duplicate key, and this surprises people in production.
    users.stream().collect(toMap(User::getRole, User::getName));
toMap
13
If two users share a role, this throws IllegalStateException: Duplicate key. It usually works in testing, where the data is tidy, and fails the first time real data contains a repeat.
IllegalStateException: Duplicate key
14
The fix is to say what should happen when two values collide:
15
    toMap(User::getRole, User::getName, (first, second) -> first)   // keep the first
    toMap(User::getRole, User::getName, (a, b) -> a + ", " + b)     // combine them
16
toMap also rejects null values. HashMap happily stores a null value; toMap throws a NullPointerException. Collecting users into a map of id to nickname fails as soon as one person has no nickname.
toMapHashMapNullPointerException
17
Do not assume the list you get back can be modified. collect(toList()) makes no promise about mutability — it happens to return a modifiable list today. Stream.toList(), added in Java 16, returns one you cannot modify. They look almost identical and behave differently.
collect(toList())Stream.toList()
18
If you need a specific kind, ask for it: toCollection(ArrayList::new).
toCollection(ArrayList::new)
19
What the interviewer is probing.1. "Group these users by department." Probing: fluency. Stalls: writing a manual loop with a map. Moves up: groupingBy, then adding a second collector when asked to count or transform.
groupingBy
20
2. "toMap threw on your data. Why?" Probing: duplicate keys. Stalls: "Bad data." Moves up: the two-argument form has no rule for collisions; supply one and decide which value wins.
toMap
21
3. "groupingBy or partitioningBy?" Probing: precision. Stalls: "They are similar." Moves up: partitioningBy takes a yes/no test and always returns both keys.
groupingBypartitioningBy
22
4. "Is the list from collect(toList()) modifiable?" Probing: the guarantee. Stalls: "Yes." Moves up: unspecified; Stream.toList() is unmodifiable, and toCollection is how you make sure.
collect(toList())Stream.toList()toCollection
23
5. "What breaks a collector in parallel?" Probing: the part sequential tests never run. Stalls: "The accumulator." Moves up: the combiner, which merges partial results and only runs when the stream is parallel.
24
  The ones worth memorising
25
  toList()                      unspecified mutability
  toSet()                       unspecified, unordered
  toCollection(TreeSet::new)    you choose the implementation
  toMap(k, v)                   THROWS on duplicate keys
  toMap(k, v, merge)            <- prefer this one
  toUnmodifiableList()          Java 10+
  joining(", ", "[", "]")       delimiter, prefix, suffix
  counting()                    Long
  summingInt / averagingDouble  numeric aggregation
  summarizingInt(...)           count, sum, min, average, max at once
  groupingBy(fn)                Map<K, List<T>>
  groupingBy(fn, downstream)    Map<K, whatever downstream gives>
  partitioningBy(pred)          Map<Boolean, List<T>>, ALWAYS both keys
  mapping(fn, downstream)       transform before collecting
  filtering(pred, downstream)   Java 9+, filter inside a grouping
  teeing(c1, c2, merger)        Java 12+, two collectors in one pass
26
  Composed examples
27
    // count per department
    groupingBy(User::getDept, counting())
28
    // names per department, sorted
    groupingBy(User::getDept,
               mapping(User::getName, toCollection(TreeSet::new)))
29
    // two levels
    groupingBy(User::getDept, groupingBy(User::getRole))
30
    // min and max in ONE pass (Java 12+)
    teeing(minBy(cmp), maxBy(cmp), (lo, hi) -> new Range(lo, hi))
31
  The duplicate-key failure
32
    // throws IllegalStateException on the second "admin"
    users.stream().collect(toMap(User::getRole, User::getName));
33
    // explicit, and correct
    users.stream().collect(toMap(User::getRole, User::getName,
                                 (first, second) -> first));