Group by Key with Stream Collectors
`Collectors.groupingBy` is the Java equivalent of SQL `GROUP BY`: pass a key extractor and you get back a `Map<K, List<T>>`. This snippet covers the basic grouping, downstream collectors (counting, summing, mapping to a different value), and multi-level grouping by chaining two `groupingBy` calls. Pair with `LinkedHashMap` when you need stable insertion order.
311 views
7
import java.util.*;
import java.util.stream.*;
public class Main {
static final class Order {
final String customer; final String product; final int qty;
Order(String c, String p, int q) { customer = c; product = p; qty = q; }
public String customer() { return customer; }
public String product() { return product; }
public int qty() { return qty; }
public String toString() { return customer + "/" + product + "x" + qty; }
}
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("ada", "book", 2),
new Order("ada", "pen", 5),
new Order("linus", "book", 1),
new Order("linus", "laptop", 1)
);
Map<String, List<Order>> byCustomer = orders.stream()
.collect(Collectors.groupingBy(Order::customer));
byCustomer.forEach((k, v) -> System.out.println(k + " -> " + v));
}
}Collectors.groupingBy(keyExtractor) walks the stream once and bins each element under the key returned by the extractor. The default downstream collector is toList, so the value type is List<T>. The result is a HashMap, which means key iteration order is not stable: pass groupingBy(key, LinkedHashMap::new, toList()) if you need insertion order. On JDK 16+ the Order class collapses to record Order(String customer, String product, int qty) {}.
import java.util.*;
import java.util.stream.*;
final class Order {
final String customer; final String product; final int qty;
Order(String c, String p, int q) { customer = c; product = p; qty = q; }
public String customer() { return customer; }
public String product() { return product; }
public int qty() { return qty; }
public String toString() { return customer + "/" + product + "x" + qty; }
}
public class Main {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("ada", "book", 2),
new Order("ada", "pen", 5),
new Order("linus", "book", 1),
new Order("linus", "laptop", 1)
);
// Count orders per customer
Map<String, Long> count = orders.stream()
.collect(Collectors.groupingBy(Order::customer, Collectors.counting()));
System.out.println("count: " + count);
// Sum quantities per customer
Map<String, Integer> qty = orders.stream()
.collect(Collectors.groupingBy(Order::customer,
Collectors.summingInt(Order::qty)));
System.out.println("qty: " + qty);
// Map values to product names per customer
Map<String, List<String>> products = orders.stream()
.collect(Collectors.groupingBy(Order::customer,
Collectors.mapping(Order::product, Collectors.toList())));
System.out.println("products: " + products);
}
}The two-arg form of groupingBy accepts a downstream collector that determines what the values look like in the result map. counting() gives Map<K, Long> (handy for histograms), summingInt(...) produces totals, and mapping(extractor, downstream) lets you transform each element before the downstream collector consumes it. Chain mapping(...) with toSet() to dedupe the values per group. These compositions replace what would otherwise be a manual loop with a computeIfAbsent + add dance.
import java.util.*;
import java.util.stream.*;
final class Order {
final String customer; final String product; final int qty;
Order(String c, String p, int q) { customer = c; product = p; qty = q; }
public String customer() { return customer; }
public String product() { return product; }
public int qty() { return qty; }
public String toString() { return customer + "/" + product + "x" + qty; }
}
public class Main {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("ada", "book", 2),
new Order("ada", "book", 3),
new Order("ada", "pen", 5),
new Order("linus", "book", 1),
new Order("linus", "laptop", 1)
);
Map<String, Map<String, Integer>> byCustomerThenProduct = orders.stream()
.collect(Collectors.groupingBy(Order::customer,
Collectors.groupingBy(Order::product,
Collectors.summingInt(Order::qty))));
byCustomerThenProduct.forEach((cust, products) -> {
System.out.println(cust + ":");
products.forEach((p, q) -> System.out.println(" " + p + " = " + q));
});
}
}Pass a second groupingBy as the downstream collector to bucket each group again by another key, yielding Map<K1, Map<K2, V>>. Here we sum quantities per (customer, product) pair, mirroring GROUP BY customer, product in SQL. The pattern composes to any depth, but readability degrades fast past two levels: at three or more, switch to a flat key (a class holding both customer and product) and group once. This is the cleanest way to compute pivot tables and category breakdowns without writing nested loops.
