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.