Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Grouping

Grouping sorts elements into buckets: all orders of one customer in one bucket, all words of one length in another. The Stream API has the Collectors.groupingBy collector for this. The result is a map where the key is the group's feature and the value is its elements.

groupingBy

Let's group words by length. The function passed to groupingBy computes the group key for each element:

void main() {
    List<String> words = List.of("cat", "sun", "road", "sky", "bridge", "moon");
 
    Map<Integer, List<String>> byLength = words.stream()
            .collect(Collectors.groupingBy(w -> w.length()));
 
    IO.println(byLength);
}

Output:

{3=[cat, sun, sky], 4=[road, moon], 6=[bridge]}

Each value of the map is the list of words of that length, in the same order as in the source data. The type of the result is Map<Integer, List<String>>.

What to do with a group

groupingBy takes another collector as the second argument — it decides what to do with the elements of each group. It is called a downstream collector. For example, instead of lists you can count or sum right away:

record Order(String customer, int amount) {}
 
void main() {
    List<Order> orders = List.of(
            new Order("Anna", 20), new Order("Oleg", 35),
            new Order("Anna", 15), new Order("Ira", 50));
 
    Map<String, Long> count = orders.stream()
            .collect(Collectors.groupingBy(Order::customer, TreeMap::new, Collectors.counting()));
    Map<String, Integer> total = orders.stream()
            .collect(Collectors.groupingBy(Order::customer, TreeMap::new, Collectors.summingInt(Order::amount)));
 
    IO.println(count);
    IO.println(total);
}

Output:

{Anna=2, Ira=1, Oleg=1}
{Anna=35, Ira=50, Oleg=35}

The middle argument TreeMap::new sets the map type. A TreeMap keeps its keys sorted, so the names come in alphabetical order.

mapping

The Collectors.mapping collector transforms the elements of a group before collecting them. This way you can collect just the amounts instead of whole orders:

record Order(String customer, int amount) {}
 
void main() {
    List<Order> orders = List.of(
            new Order("Anna", 20), new Order("Oleg", 35), new Order("Anna", 15));
 
    Map<String, List<Integer>> amounts = orders.stream()
            .collect(Collectors.groupingBy(
                    Order::customer,
                    TreeMap::new,
                    Collectors.mapping(Order::amount, Collectors.toList())));
 
    IO.println(amounts);
}

Output:

{Anna=[20, 15], Oleg=[35]}

partitioningBy

partitioningBy is a special case of grouping with exactly two groups, true and false. The key is the result of a condition:

void main() {
    List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
 
    Map<Boolean, List<Integer>> parts = numbers.stream()
            .collect(Collectors.partitioningBy(n -> n % 2 == 0));
 
    IO.println("Even: " + parts.get(true));
    IO.println("Odd: " + parts.get(false));
}

Output:

Even: [2, 4, 6]
Odd: [1, 3, 5, 7]

Unlike groupingBy, both groups are always in the map — even if one of them is empty.

Walking through the result

The result of grouping is an ordinary map, so you can walk through it with entrySet() or forEach:

void main() {
    List<String> fruits = List.of("apple", "pear", "apricot", "blueberry", "banana");
 
    fruits.stream()
            .collect(Collectors.groupingBy(f -> f.charAt(0), TreeMap::new, Collectors.toList()))
            .forEach((letter, list) -> IO.println(letter + ": " + list));
}

Output:

a: [apple, apricot]
b: [blueberry, banana]
p: [pear]

Common mistake

By default groupingBy puts the groups into a HashMap, which promises no key order at all. Code can print groups in the "right" order for years and then shuffle them after the data or the Java version changes:

void main() {
    List<String> names = List.of("Oleg", "Anna", "Ira", "Boris", "Vera", "Alla");
 
    IO.println(names.stream().collect(Collectors.groupingBy(n -> n.substring(0, 2))));
}

Output (the key order is not guaranteed):

{Ir=[Ira], Al=[Alla], Bo=[Boris], An=[Anna], Ol=[Oleg], Ve=[Vera]}

If the order matters, pass the map factory explicitly: TreeMap::new to sort by key or LinkedHashMap::new to keep the order of appearance.

Thanks for reading!

Donate to the site — we work hard for you.