🌊 Streams API · Intermediate

groupingBy & partitioningBy in Java

Building maps of groups with downstream collectors.

🧩 The mysteryGroup numbers by "is it odd?" and ask for the odd group: one way gives you null, another gives []. Same question, two answers — which collector is which?

Sorting mail into pigeonholes

groupingBy(keyFn) puts every element into a pigeonhole by its key and returns a **Map<K, List<T>>. By default it's a HashMap, so the keys have no guaranteed order. Need sorted keys? Pass TreeMap::new**.

Downstream collectors

A downstream collector decides what each group becomes: **counting()** → Long, **summingInt(f)** → Integer, **averagingInt(f)** → Double, plus mapping, toSet… Inside a group, elements keep their encounter order.

Map<String, Integer> total = emps.stream()
    .collect(Collectors.groupingBy(
        Emp::dept,
        Collectors.summingInt(Emp::salary)));
🔮 Predict it

Count by first letter

What does this print?

var m = Stream.of("ant", "ape", "bee", "cow")
    .collect(Collectors.groupingBy(
        s -> s.charAt(0), TreeMap::new,
        Collectors.counting()));
System.out.println(m);
  1. {a=[ant, ape], b=[bee], c=[cow]}
  2. {a=2, b=1, c=1}
  3. {c=1, b=1, a=2}
Show the answer

{a=2, b=1, c=1} — counting() turns each group into its size, and TreeMap::new sorts the keys.

partitioningBy: exactly two buckets

partitioningBy(predicate) is the yes/no special case: the map **always has both keys, true and false** — even if a bucket is empty. It accepts downstream collectors too.

var p = Stream.of(1, 2, 3, 4, 5)
    .collect(Collectors.partitioningBy(
        n -> n > 3, Collectors.counting()));
// {false=3, true=2}
🔮 Predict it

Nobody passes

What does this print?

var p = Stream.of(1, 3, 5)
    .collect(Collectors.partitioningBy(
        n -> n > 10));
System.out.println(p);
  1. {false=[1, 3, 5]}
  2. {false=[1, 3, 5], true=[]}
  3. {true=null, false=[1, 3, 5]}
Show the answer

{false=[1, 3, 5], true=[]} — no number is > 10, but partitioningBy **still creates the true key**, with an empty list.

Asking for a group that doesn't exist

✗ groupingBy: null
var g = Stream.of(2, 4).collect(
    Collectors.groupingBy(
        n -> n % 2 == 1));
g.get(true); // null

groupingBy only creates keys that actually occur. A missing key gives null — a NullPointerException waiting to happen.

✓ partitioningBy: []
var p = Stream.of(2, 4).collect(
    Collectors.partitioningBy(
        n -> n % 2 == 1));
p.get(true); // []

Both keys always exist, so you get an empty list instead.

💼 In the real world

Reports in one expression

"Revenue per region", "orders per status", "passed vs failed tests" — grouping is how dashboards and reports get built. Teams that print such maps in emails or logs pass TreeMap::new so the output order doesn't change between runs.

Key takeaways

  1. groupingBy(key) → Map<K, List<T>> (a HashMap, unordered)
  2. groupingBy(key, TreeMap::new, downstream) for sorted keys
  3. Downstream: counting(), summingInt(), mapping(), toSet()…
  4. partitioningBy always has both true and false keys
🤯 Did you know?

groupingBy is essentially SQL's GROUP BY in Java form — downstream collectors play the role of COUNT, SUM and AVG.

Practice questions

What does this print?

Map<Integer, List<String>> byLen =
    Stream.of("cat", "ox", "dog", "be")
        .collect(Collectors.groupingBy(
            String::length, TreeMap::new,
            Collectors.toList()));
System.out.println(byLen);
  1. {2=[ox, be], 3=[cat, dog]}
  2. {3=[cat, dog], 2=[ox, be]}
  3. {2=2, 3=2}
  4. {2=[be, ox], 3=[cat, dog]}
Check your answer

{2=[ox, be], 3=[cat, dog]}. Elements are grouped by length, the TreeMap sorts the keys, and each list keeps the encounter order of its elements.

What does this print?

Map<Boolean, Long> p = Stream.of(1, 2, 3, 4, 5)
    .collect(Collectors.partitioningBy(
        n -> n > 3, Collectors.counting()));
System.out.println(p.get(true) + " " + p.get(false));
  1. 3 2
  2. 2 3
  3. 4 5
  4. 5 0
Check your answer

2 3. 4 and 5 are > 3 (2 of them); 1, 2, 3 are not (3 of them). counting is the downstream collector applied to each partition.

Next: why Stream<Integer> has no sum() method — and the primitive streams that do.