🌊 Streams API · Intermediate

Collectors basics in Java

toList, toSet, toMap (duplicate key exception), joining, counting.

🧩 The mysteryYou map words to their lengths with 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.

🔮 Predict it

Joining strings

What does this print?

String s = Stream.of("x", "y", "z")
    .collect(Collectors.joining(
        "-", "<", ">"));
System.out.println(s);
  1. <x>-<y>-<z>
  2. <x-y-z>
  3. 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 2

toMap: 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 trap

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));
// IllegalStateException
🔮 Predict it

Merge 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=1, 2=1}
  2. {1=1, 2=2}
  3. 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.

💼 In the real world

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

  1. toSet() removes duplicates (no order guarantee)
  2. joining(sep, prefix, suffix) builds a String
  3. toMap(key, value) throws IllegalStateException on duplicate keys
  4. toMap(key, value, merge, TreeMap::new) fixes both issues
🤯 Did you know?

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);
  1. a, b, c
  2. [a], [b], [c]
  3. [a, b, c]
  4. [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);
  1. {2=ab}
  2. {2=cd}
  3. {2=ab, 2=cd}
  4. 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.

Next: groupingBy turns a flat list into a map of groups — sorting mail into pigeonholes.