Back to Stream: Sorting & Comparator
Q6Question 6 of 23

Group employees by department into a sorted TreeMap using groupingBy with a map supplier

Key Concepts
  • The three-argument `groupingBy(classifier, mapFactory, downstream)` lets you pick both the map type and how each group is reduced.
  • Passing `TreeMap::new` as the map factory returns keys sorted by natural order, instead of the unordered default `HashMap`.
  • The one-argument form returns `Map<K, List<T>>` backed by a `HashMap` with no key order.
  • The downstream collector (for example `toList`, `counting`, `mapping`) decides what each group's value becomes.
  • A custom key order comes from `() -> new TreeMap<>(comparator)` as the map factory.
  • `LinkedHashMap::new` preserves the encounter order of the first appearance of each key.
  • Sorting the keys via the map factory needs no separate sort step, unlike sorting a `HashMap`'s entries afterward.

Definition

`groupingBy(classifier, TreeMap::new, downstream)` groups into a map whose keys stay sorted, instead of the unordered default `HashMap`.

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::new for sorted keys or LinkedHashMap::new for insertion order.
  • downstream collector – reduces each group's elements into the map value, for example toList(), counting(), or mapping(...).
  • default form – the one-argument groupingBy uses a HashMap and toList, 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.

JAVA
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());
    }
}
TEXT
[Risk, Trading, Management]   // HashMap: order is unspecified

The Employee record:

JAVA
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 |

JAVA
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.

JAVA
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()));
    }
}
TEXT
{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.

JAVA
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()));
TEXT
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.

JAVA
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()));
TEXT
{Trading=[...], Risk=[...], Management=[...]}

How do the map-factory choices compare?

DIAGRAM
| 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 |
JAVA
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.

JAVA
// 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
JAVA
// 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.

JAVA
// WRONG: null department becomes a null key -> TreeMap throws NPE
employees.stream().collect(groupingBy(Employee::department, TreeMap::new, toList()));
JAVA
// 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.

JAVA
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

DIAGRAM
  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=[...] }
DIAGRAM
  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│
  └───────────────┴───────────────────────────┘
TEXT
  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.

---

Share
Use ← → arrow keys to navigate