Grouping Objects by Field with Collectors.groupingBy — Single, Multi-level, Downstream
Fast recall
Quick revision
Core answer
Map> keyed by the classifier function: users.stream().collect(groupingBy(User::getDepartment)). The two-arg overload groupingBy(classifier, downstream) lets you replace the default toList() with another collector — counting(), summingInt(...), averagingDouble(...), mapping(...), reducing(...), or another nested groupingBy(...) for multi-level grouping (e.g., groupingBy(User::getDept, groupingBy(User::getRole)) returns Map>> ). The three-arg overload groupingBy(classifier, mapFactory, downstream) lets you choose a specific Map impl — LinkedHashMap::new for insertion order, TreeMap::new for sorted keys. Common patterns: count per group (groupingBy(k, counting())), sum per group (groupingBy(k, summingInt(User::getAge))), list of one field per group (groupingBy(k, mapping(User::getEmail, toList()))), boolean partition (use `partitioningBy(predicate)` which returns Map> — faster than groupingBy for two-bucket cases). Watch out for: null keys (groupingBy throws NPE on null classifier output — use Optional.ofNullable(...).orElse("unknown")), mutability (default returned List and Map are mutable; wrap in Collections.unmodifiableMap if you need immutable), and parallel streams (use groupingByConcurrent for concurrent collection if you actually parallelize). For SQL-like aggregation pipelines, this is the workhorse — it replaces nested for loops + manual HashMap.computeIfAbsent with one declarative line.Whether the candidate knows the three overloads of `groupingBy`, the common downstream collectors (counting, summing, mapping, reducing), the `partitioningBy` shortcut for booleans, and the null-key gotcha
Using `Collectors.toMap` for grouping (loses duplicates), forgetting that `groupingBy` throws NPE on null classifier output, not knowing the `mapping` downstream collector for transforming grouped elements
Mention `groupingByConcurrent` for parallel streams, the `mapFactory` arg for `LinkedHashMap`/`TreeMap` ordering, multi-level grouping with nested `groupingBy`, and `teeing` (Java 12+) for two parallel aggregations
Interview-ready guide
Interview answer
Use `Collectors.groupingBy(classifier)` to group a stream into a Map. The classifier is a function from element → key.
Three overloads
groupingBy(classifier)→Map, default downstream is> toList()groupingBy(classifier, downstream)→ use any collector for the values:counting,summingInt,mapping,reducing, nestedgroupingBy...groupingBy(classifier, mapFactory, downstream)→ pick the Map impl:LinkedHashMap,TreeMap, etc.
Common downstream collectors
counting()→ count per groupsummingInt(f)/summingLong/summingDouble→ sumaveragingInt(f)→ averageminBy(c)/maxBy(c)→ min/max element per groupmapping(mapper, downstream)→ transform elements before collecting (e.g., extract names)summarizingInt(f)→ IntSummaryStatistics with count/sum/min/max/avgreducing(identity, mapper, op)→ custom reduction
Multi-level grouping — nest groupingBy as downstream
groupingBy(dept, groupingBy(role))→Map>> groupingBy(dept, groupingBy(role, counting()))→Map> - Maps to SQL
GROUP BY dept, role
Map impl matters
- Default
HashMap— no order LinkedHashMap::new— insertion order (encounter order of first element per group)TreeMap::new— sorted by keygroupingByConcurrentfor parallel streams writing toConcurrentHashMap
`partitioningBy` for booleans
partitioningBy(predicate)returnsMap> - Both true and false keys always present, even if one is empty
- Faster than
groupingByfor two-bucket cases - Communicates intent better
Pitfalls
1. Null classifier output throws NPE
groupingBy(User::dept)fails if any user.dept() is null- Wrap in
Optional.ofNullable(...)or default to a sentinel string
2. Don't use `toMap` for grouping
toMap(User::dept, u -> u)throwsIllegalStateExceptionon duplicate key- For one-to-many — always use
groupingBy - If you need
toMap, provide a merge function:(existing, dup) -> existing
3. Mutability
- Default returns mutable
HashMap+ArrayList - Wrap with
collectingAndThen(..., Collections::unmodifiableMap)for immutable
4. Parallel performance
groupingByworks in parallel via thread-local maps + mergegroupingByConcurrentwrites directly to a sharedConcurrentHashMap— faster for very large N with many groups
`teeing` (Java 12+)
- Combine two aggregates in one stream pass
- E.g., count + sum to compute custom average
teeing(counting(), summingInt(f), Stats::new)
SQL analogy
groupingBy=GROUP BY- downstream collectors = aggregate functions
- Multi-level groupingBy =
GROUP BY a, b partitioningBy(p)=GROUP BY (CASE WHEN p THEN 1 ELSE 0 END)
Performance
- Negligible overhead for typical N
- For hot paths or millions of elements, profile
- For very-parallel workloads,
groupingByConcurrent - For primitive aggregation,
IntStream.summaryStatistics()may be faster than collector chains
Recommendation: Reach for `Collectors.groupingBy(classifier, downstream)` as your default for any 'group X by Y, then aggregate Z' pattern — it replaces 5–10 lines of imperative computeIfAbsent loops with one declarative line. Use the two-arg form with `counting`, `summingInt`, `mapping`, `reducing` for aggregates; use nested `groupingBy` for multi-level grouping (limit to 2–3 levels for readability). Prefer `partitioningBy(predicate)` over groupingBy whenever the classifier is boolean — it's faster and both keys are guaranteed present. Pick the mapFactory (LinkedHashMap, TreeMap) when iteration order or sorted output matters. Always handle null classifier outputs with Optional.ofNullable or a sentinel — default groupingBy throws NPE on null keys. Don't confuse with `toMap` — use toMap for unique keys (with a merge function for safety) and groupingBy for one-to-many. For parallel large-data scenarios, switch to `groupingByConcurrent`. And remember `teeing` (Java 12+) for two-aggregate single-pass collection. The SQL mental model — GROUP BY + aggregate functions — maps cleanly: it's the same operation, just expressed in Java's collector grammar.
Grouping Objects by Field with `Collectors.groupingBy` — Single, Multi-level, Downstream
Direct Answer
Use Collectors.groupingBy(classifier) to group a stream of objects into a Map<K, List<T>> keyed by the classifier function: users.stream().collect(groupingBy(User::getDepartment)). The two-arg overload groupingBy(classifier, downstream) lets you replace the default toList() with another collector — counting(), summingInt(...), averagingDouble(...), mapping(...), reducing(...), or another nested groupingBy(...) for multi-level grouping (e.g., groupingBy(User::getDept, groupingBy(User::getRole)) returns Map<Dept, Map<Role, List<User>>>). The three-arg overload groupingBy(classifier, mapFactory, downstream) lets you choose a specific Map impl — LinkedHashMap::new for insertion order, TreeMap::new for sorted keys. Common patterns: count per group (groupingBy(k, counting())), sum per group (groupingBy(k, summingInt(User::getAge))), list of one field per group (groupingBy(k, mapping(User::getEmail, toList()))), boolean partition (use partitioningBy(predicate) which returns Map<Boolean, List<T>> — faster than groupingBy for two-bucket cases). Watch out for: null keys (groupingBy throws NPE on null classifier output — use Optional.ofNullable(...).orElse("unknown")), mutability (default returned List and Map are mutable; wrap in Collections.unmodifiableMap if you need immutable), and parallel streams (use groupingByConcurrent for concurrent collection if you actually parallelize). For SQL-like aggregation pipelines, this is the workhorse — it replaces nested for loops + manual HashMap.computeIfAbsent with one declarative line.
Common Mistakes & Pitfalls
Using Collectors.toMap for grouping (loses duplicates), forgetting that groupingBy throws NPE on null classifier output, not knowing the mapping downstream collector for transforming grouped elements
Check your understanding
Answer these without looking back at the article.
- How do you handle null keys when using `groupingBy`?
- What's the difference between `groupingBy` and `partitioningBy`?
- When should you use `groupingByConcurrent` instead of `groupingBy`?
- How would you compute the average age per department in one pass?
- What does the `mapping` downstream collector do?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.