streams
intermediateCollectors
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 String2
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 department5
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 department7
groupingBy(User::getDept, mapping(User::getName, toList()))
// Map<Dept, List<String>> -- just the names8
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 present11
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 them16
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 memorising25
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 pass26
Composed examples27
// 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 failure32
// 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));