Grouping and partitioning
Manual grouping with computeIfAbsent, stream groupingBy downstream collectors, map suppliers, partitioningBy, multi-level groups, complexity and pitfalls.
On this page
- Picture it first: buckets with a calculator attached
- Grouping under the hood: the loop version
- Stream grouping: downstream collectors do the real work
- Choosing the map: order is part of the contract
- Partitioning: boolean grouping with both sides present
- Advanced: applications, concurrency and pitfalls
- How to say it in the interview
🪣 What are grouping and partitioning?
Grouping means taking a flat stream of items and putting each item into a bucket chosen by a classifier, such as customerId, department or logLevel. Partitioning is the two-bucket version where the classifier is a boolean, like paid versus unpaid. It acts like sorting mail into apartment mailboxes: every envelope follows one address rule, then each mailbox may keep the envelopes, count them or add up their value.
For this lesson, we'll focus on the grouping pattern you should recognize in interviews and code reviews: manual grouping with computeIfAbsent, stream grouping with downstream collectors, multi-level grouping, explicit map choices, partitioningBy, and the edge cases around nulls, mutability and output order. Interviewers ask this because the easy version is ten lines, but the real signal is whether you can choose the right bucket value, not always List<T>.
Below the line (out of scope)
- Hash table mechanics behind the buckets: How HashMap works
- Counting without storing grouped objects: Frequency counting and deduplication
- Choosing
LinkedHashMap,TreeMapandEnumMap: WeakHashMap, IdentityHashMap and EnumMap and TreeMap and red-black trees - Parallel stream mechanics and concurrent collectors are mentioned here, but covered properly in the Concurrency module.
The 30-second answer
Grouping is Map<K, List<V>> when you need the original items by key, and Map<K, Aggregate> when you only need counts, sums, averages, sets or a best item per key. In a loop, I use computeIfAbsent(key, k -> new ArrayList<>()).add(item) because it creates each bucket exactly once and returns the bucket to mutate. In streams, Collectors.groupingBy(classifier, downstream) is the readable option, and the three-argument form lets me choose TreeMap::new, LinkedHashMap::new or an EnumMap supplier. partitioningBy is boolean grouping and always gives both true and false keys. Complexity is usually expected O(n) time plus the downstream work, with O(n) space if you keep lists and O(g) if you keep one aggregate per group.
What you’ll learn
- Build manual groups with
computeIfAbsentwithout the old get-null-put boilerplate - Use
Collectors.groupingBywith downstream collectors likecounting,summingInt,mapping,maxByandcollectingAndThen - Choose a result map with the **three-argument
groupingBy** overload:TreeMap,LinkedHashMaporEnumMap - Model partitioning as boolean grouping with
partitioningByand both boolean keys present - Explain multi-level grouping,
groupingByConcurrent, null-key behavior and mutable result pitfalls - Compare O(n) list grouping with O(g) aggregate grouping for counts and totals
01Picture it first: buckets with a calculator attached
Picture a conveyor belt of orders. A small label maker reads each order and prints the bucket key, maybe the customer ID. The order drops into that customer's bucket, and a small calculator attached to the bucket either stores the order, increments a count, adds the amount or keeps the largest order seen so far.
That is the whole model. The classifier chooses the bucket; the downstream collector chooses what lives inside it.
Orders flow through a classifier into customer buckets, then a counting downstream collector produces c-1 to 2, c-2 to 1 and c-3 to 3
Notice the two separate decisions. SalesOrder::customerId is not the same choice as Collectors.counting(). If you keep those separate in your head, groupingBy stops looking like magic and starts looking like a map-building loop.
02Grouping under the hood: the loop version
Try it yourself first
Before reading the code, group these orders by customer in your head: c-1:1200, c-2:900, c-1:300. Which list is created once and then reused?
Here's how I'd write the manual version when validation, logging or early exits make a loop clearer than a stream.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
record Order(String id, String customerId, int cents) { }
final class ManualGroupingDemo {
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("o-1", "c-1", 1200),
new Order("o-2", "c-2", 900),
new Order("o-3", "c-1", 300)
);
Map<String, List<Order>> byCustomer = new HashMap<>();
for (Order order : orders) {
byCustomer
.computeIfAbsent(order.customerId(), key -> new ArrayList<>())
.add(order);
}
System.out.println(byCustomer.get("c-1").size()); // 2
System.out.println(byCustomer.get("c-2").size()); // 1
}
}
Notice three things:
computeIfAbsentcreates theArrayListonly when the customer is first seen.- It returns the stored list, so the chained
.add(order)mutates the bucket in the map. - The map stores one list per customer, not one list per order.
The old get, null-check and put version works, but it spreads one idea across five lines. It is also easy to get subtly wrong: forgetting the put, adding to a local list that never reaches the map, or doing the lookup twice with a classifier that is more expensive than it looks.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Stream grouping: downstream collectors do the real work
- Choosing the map: order is part of the contract
- Partitioning: boolean grouping with both sides present
- Advanced: applications, concurrency and pitfalls
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal