Definition
The three-argument Collectors.groupingBy has three independent parts.
- classifier – a function that extracts the grouping key from each element, for example
Employee::getDepartment. - map factory – a supplier that creates the result map, for example
TreeMap::newfor sorted keys orLinkedHashMap::newfor insertion order. - downstream collector – reduces each group's elements into the map value, for example
toList(),counting(), ormapping(...). - default form – the one-argument
groupingByuses aHashMapandtoList, giving no key order.
Priya sorts employee files into labelled trays by department. The ordinary way throws the trays onto the shelf in no particular order (a HashMap). The sorted way (a TreeMap) keeps the trays in alphabetical order by department label, so Risk always sits after Management without her rearranging anything.
Understanding the Concepts
What is Collectors.groupingBy?
groupingBy is a collector that partitions the elements of a stream into groups keyed by a classifier function, returning a Map from each key to the elements that fall under it. It is the stream equivalent of a SQL GROUP BY.
package com.bank.report;
import com.bank.model.Employee;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.groupingBy;
public class GroupingDemo {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee(1, "Rajesh", "Trading", new BigDecimal("1200000")),
new Employee(2, "Priya", "Risk", new BigDecimal("850000")),
new Employee(3, "Pankaj", "Management", new BigDecimal("2000000"))
);
Map<String, List<Employee>> byDept = employees.stream()
.collect(groupingBy(Employee::department));
System.out.println(byDept.keySet());
}
}
[Risk, Trading, Management] // HashMap: order is unspecified
The Employee record:
package com.bank.model;
import java.math.BigDecimal;
public record Employee(long id, String name, String department, BigDecimal salary) { }
What are the three overloads of groupingBy?
groupingBy has three forms that build on each other. The one-argument form groups into lists in a HashMap. The two-argument form adds a downstream collector. The three-argument form inserts a map factory between the classifier and the downstream.
| Overload | Signature shape | Result | Key order |
| --- | --- | --- | --- |
| 1-arg | groupingBy(classifier) | Map | none (HashMap) |
| 2-arg | groupingBy(classifier, downstream) | Map | none (HashMap) |
| 3-arg | groupingBy(classifier, mapFactory, downstream) | M extends Map | decided by factory |
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
// 1-arg: Map<String, List<Employee>> in a HashMap
Map<String, List<Employee>> a = employees.stream()
.collect(groupingBy(Employee::department));
// 2-arg: Map<String, Long> count per department, still a HashMap
Map<String, Long> b = employees.stream()
.collect(groupingBy(Employee::department, counting()));
How do you group into a sorted TreeMap?
Pass TreeMap::new as the map factory in the three-argument form. The resulting TreeMap keeps keys in natural order, so departments come out alphabetically with no extra sort step.
package com.bank.report;
import com.bank.model.Employee;
import java.util.List;
import java.util.TreeMap;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
public class SortedGrouping {
public TreeMap<String, List<Employee>> byDepartmentSorted(List<Employee> employees) {
return employees.stream()
.collect(groupingBy(Employee::department, TreeMap::new, toList()));
}
}
{Management=[...], Risk=[...], Trading=[...]} // TreeMap: alphabetical keys
How do you choose the downstream collector?
The downstream collector decides what each group's value becomes. toList() keeps the elements. counting() gives a count. mapping(...) transforms each element before collecting. averagingDouble(...) or summingDouble(...) aggregate numbers. You can combine a sorted key map with any of these.
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.counting;
// Sorted keys, values are employee names only
TreeMap<String, List<String>> names = employees.stream()
.collect(groupingBy(Employee::department, TreeMap::new,
mapping(Employee::name, toList())));
// Sorted keys, values are head counts
TreeMap<String, Long> headcount = employees.stream()
.collect(groupingBy(Employee::department, TreeMap::new, counting()));
names = {Management=[Pankaj], Risk=[Priya], Trading=[Rajesh]}
headcount = {Management=1, Risk=1, Trading=1}
How do you sort keys with a custom comparator?
A bare TreeMap::new uses natural order. For a custom order, supply a lambda that builds a TreeMap with your comparator, for example descending order or case-insensitive order.
import java.util.Comparator;
import java.util.TreeMap;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
// Departments in reverse alphabetical order
TreeMap<String, List<Employee>> reverse = employees.stream()
.collect(groupingBy(Employee::department,
() -> new TreeMap<>(Comparator.reverseOrder()),
toList()));
{Trading=[...], Risk=[...], Management=[...]}
How do the map-factory choices compare?
| Map factory | Key order | Typical use | | --- | --- | --- | | `HashMap::new` (default) | unspecified | best throughput, order irrelevant | | `TreeMap::new` | natural sort order | alphabetical or numeric key listing | | `() -> new TreeMap<>(cmp)` | custom sort order | reverse or case-insensitive keys | | `LinkedHashMap::new` | first-seen insertion order | preserve the order keys appeared |
import java.util.LinkedHashMap;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
// Preserve the order in which departments first appeared in the stream
LinkedHashMap<String, List<Employee>> insertionOrder = employees.stream()
.collect(groupingBy(Employee::department, LinkedHashMap::new, toList()));
What are common mistakes?
The first mistake is grouping into a HashMap and then being surprised the keys are unordered, or sorting the entries afterward when a TreeMap factory would have done it in one step.
// WRONG style: group then sort the keys separately
Map<String, List<Employee>> hash = employees.stream()
.collect(groupingBy(Employee::department));
List<String> sortedKeys = hash.keySet().stream().sorted().toList(); // extra pass
// CORRECT: sorted keys straight from the collector
TreeMap<String, List<Employee>> sorted = employees.stream()
.collect(groupingBy(Employee::department, TreeMap::new, toList()));
The second mistake is a classifier that returns null, which a TreeMap rejects with a NullPointerException because it cannot order a null key, even though a HashMap would have allowed one null key.
// WRONG: null department becomes a null key -> TreeMap throws NPE
employees.stream().collect(groupingBy(Employee::department, TreeMap::new, toList()));
// CORRECT: normalize the key so there is no null
employees.stream().collect(groupingBy(
e -> e.department() == null ? "UNKNOWN" : e.department(),
TreeMap::new, toList()));
What are edge cases and senior nuances?
A senior point is that the map factory is a deliberate design choice, not a detail. TreeMap gives sorted keys and accepts a custom comparator, LinkedHashMap preserves first-seen order, and HashMap gives the best throughput when order does not matter. Because the downstream collector is fully composable, you can nest collectors to build richer values, for example a sorted map of department to the average salary.
import java.util.TreeMap;
import static java.util.stream.Collectors.averagingDouble;
import static java.util.stream.Collectors.groupingBy;
// Sorted department -> average salary
TreeMap<String, Double> avgByDept = employees.stream()
.collect(groupingBy(Employee::department, TreeMap::new,
averagingDouble(e -> e.salary().doubleValue())));
Another nuance is that groupingBy is not concurrent; for a large parallel stream use groupingByConcurrent with a ConcurrentHashMap, but note that drops key ordering, so you would sort afterward if you also needed sorted keys.
When should you use the three-argument form?
Use the three-argument groupingBy whenever you care about the type or ordering of the result map, or need a non-list value per group. Reach for TreeMap::new when a report must display groups in a stable, sorted order on every load, which is exactly what an org-chart or statement screen needs. Stick with the one-argument form when order is irrelevant and lists per key are enough.
Visual Overview
groupingBy(classifier, mapFactory, downstream)
elements
│ classifier: Employee::getDepartment
▼
┌──────────────┬──────────────┬──────────────┐
│ Management │ Risk │ Trading │ <- keys
│ [Pankaj] │ [Priya] │ [Rajesh] │ <- downstream toList()
└──────────────┴──────────────┴──────────────┘
│ mapFactory: TreeMap::new -> keys kept sorted
▼
{ Management=[...], Risk=[...], Trading=[...] }
Map factory decides key order ┌───────────────┬───────────────────────────┐ │ HashMap::new │ Trading, Risk, Management │ (unspecified) ├───────────────┼───────────────────────────┤ │ TreeMap::new │ Management, Risk, Trading │ (natural sort) ├───────────────┼───────────────────────────┤ │ TreeMap(cmp) │ Trading, Risk, Management │ (reverse) ├───────────────┼───────────────────────────┤ │ LinkedHashMap │ first-seen insertion order│ └───────────────┴───────────────────────────┘
Downstream collector shapes the VALUE
toList() -> Map<Dept, List<Employee>>
counting() -> Map<Dept, Long>
mapping(name) -> Map<Dept, List<String>>
averaging() -> Map<Dept, Double>
Key Takeaway
groupingBy has three overloads, and the three-argument form is the one I reach for when I care about the type and ordering of the result map. It takes the classifier that decides the key, a map factory that chooses the map implementation, and a downstream collector that reduces each group. By passing TreeMap::new as the factory I get a map whose department keys stay sorted in natural order, instead of the arbitrary order of the default HashMap.
In my banking work at the National Bank of India the org-chart screen grouped employees by department and had to show departments alphabetically so the page looked the same on every load. I used groupingBy with the department as the classifier, TreeMap::new as the factory, and a mapping downstream that collected just the employee names, and the result came out already sorted by department with no separate sort step.
The senior point I always add is that the map factory is a deliberate design choice. TreeMap gives sorted keys and can take a custom comparator, LinkedHashMap preserves the order in which keys first appear, and the default HashMap gives the best throughput when order does not matter. Because the downstream collector is fully composable, I can combine sorted keys with counting, averaging, or mapping in the same call. The one gotcha I watch for is a null key, which a TreeMap rejects, so I normalize the classifier to a safe default first.
Think of Priya filing employee folders into labelled trays: the TreeMap keeps those trays alphabetical on the shelf automatically, so Management always sits before Risk before Trading.
Remember one sentence: the three-argument groupingBy lets you pick the map type with a factory like TreeMap::new for sorted keys and the downstream collector for each group's value.
---