Collectors basics in Java
toList, toSet, toMap (duplicate key exception), joining, counting.
toMap. Works perfectly in testing… then production data has two 5-letter words and the whole request crashes. Why?A recipe for a container
collect(...) takes a Collector — a recipe for building a result container. The Collectors class is the recipe book: **toList, toSet, toMap, joining, counting** and more.
Joining strings
What does this print?
String s = Stream.of("x", "y", "z")
.collect(Collectors.joining(
"-", "<", ">"));
System.out.println(s);<x>-<y>-<z><x-y-z>x-y-z
Show the answer
<x-y-z> — joining(separator, prefix, suffix) puts - between elements and wraps the whole string once.
toSet and counting
**toSet() removes duplicates and gives no order guarantee. counting()** counts every element and returns a Long.
Stream.of("x", "y", "x")
.collect(Collectors.counting()); // 3
Stream.of("x", "y", "x")
.collect(Collectors.toSet()); // size 2toMap: key and value functions
toMap(keyFn, valueFn) builds a map entry for every element. Every key must be unique.
Map<String, Integer> lengths = words.stream()
.collect(Collectors.toMap(
w -> w, String::length));The duplicate-key crash
If two elements produce the same key, toMap can't guess which value wins, so it throws **IllegalStateException** ("Duplicate key"). Here both words have length 2.
Stream.of("ab", "cd")
.collect(Collectors.toMap(
String::length, s -> s));
// IllegalStateExceptionMerge function to the rescue
A 3rd argument merges clashing values; a 4th picks the map type. What prints?
var m = Stream.of("a", "bb", "cc")
.collect(Collectors.toMap(
String::length, s -> 1,
Integer::sum, TreeMap::new));
System.out.println(m);{1=1, 2=1}{1=1, 2=2}Throws IllegalStateException
Show the answer
{1=1, 2=2} — the merge function adds clashing values, so length 2 counts both words. TreeMap::new sorts the keys for a predictable result.
Production crash, classic edition
"Duplicate key" from toMap is one of the most common stream exceptions in production logs: test data had unique keys, real data didn't. Ask yourself "can two elements share a key?" — if yes, pass a merge function.
Key takeaways
- toSet() removes duplicates (no order guarantee)
- joining(sep, prefix, suffix) builds a String
- toMap(key, value) throws IllegalStateException on duplicate keys
- toMap(key, value, merge, TreeMap::new) fixes both issues
toMap's error message is surprisingly helpful: it names the duplicate key *and* both clashing values, e.g. Duplicate key 2 (attempted merging values ab and cd).
Practice questions
What does this print?
String s = Stream.of("a", "b", "c")
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(s);- a, b, c
- [a], [b], [c]
- [a, b, c]
- [a,b,c]
Check your answer
[a, b, c]. joining(separator, prefix, suffix) puts ", " between elements and wraps the whole string once in [ and ].
What does this print?
Map<Integer, String> byLen = Stream.of("ab", "cd")
.collect(Collectors.toMap(
String::length, s -> s));
System.out.println(byLen);- {2=ab}
- {2=cd}
- {2=ab, 2=cd}
- Throws IllegalStateException
Check your answer
Throws IllegalStateException. Both strings have length 2, so both map to key 2. toMap without a merge function refuses to guess and throws.