Back to Stream: Sorting & Comparator
Q9Question 9 of 23

Group employees by city and sort each group by salary descending

Key Concepts
  • groupingBy(city, collectingAndThen(toList(), finisher)) groups by city and post-processes each list
  • The finisher sorts each city's list in place using comparingDouble(salary).reversed()
  • reversed() flips ascending to descending so the highest paid appears first in each group
  • collectingAndThen is the general tool for any post-collection processing: sorting, trimming, immutability
  • Prefer comparingDouble(...).reversed() over negating salary values for clarity and correctness
  • The result is Map<String, List<Employee>> with each list ordered highest to lowest salary

Definition

groupingBy(city, collectingAndThen(toList(), sort by salary desc)) buckets by city and orders each bucket highest first.
  • groupingBy(classifier, downstream) partitions elements by city.
  • collectingAndThen(toList(), finisher) collects each group into a list, then applies a finishing function.
  • The finisher sorts the list with comparingDouble(salary).reversed() for descending order.
  • reversed() flips the natural ascending comparator to descending.
  • The result is Map> with each list sorted highest salary first.

Priya at the National Bank of India sorts employees into city trays, then within each tray she arranges them top earner first. Two steps: bucket by city, then order inside each bucket.

Understanding the Concepts

How do you group by city and sort each group?

You group by city with groupingBy, and the downstream collector first collects each group into a list, then sorts it. collectingAndThen wraps the collection and sorting into a single downstream.

JAVA
package com.bank.hr;

import java.util.*;
import java.util.stream.Collectors;

public class GroupAndSort {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Rajesh", "Mumbai",  75000.00),
            new Employee("Priya",  "Mumbai",  82000.00),
            new Employee("Amit",   "Delhi",   68000.00),
            new Employee("Kavita", "Delhi",   91000.00),
            new Employee("Neha",   "Delhi",   78000.00)
        );

        Map<String, List<Employee>> sorted = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getCity,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    list -> {
                        list.sort(Comparator.comparingDouble(
                            Employee::getSalaryAsDouble).reversed());
                        return list;
                    })));

        sorted.forEach((city, emps) -> {
            System.out.println(city + ":");
            emps.forEach(e -> System.out.println(
                "  " + e.getName() + " - " + e.getSalaryAsDouble()));
        });
    }
}
TEXT
Mumbai:
  Priya - 82000.0
  Rajesh - 75000.0
Delhi:
  Kavita - 91000.0
  Neha - 78000.0
  Amit - 68000.0

What is collectingAndThen?

collectingAndThen takes a collector and a finishing function. It first collects using the inner collector, then applies the finishing function to the result.

| Use case | Inner collector | Finisher |
|---|---|---|
| Sort a group | toList() | list.sort(...); return list |
| Make group immutable | toList() | Collections.unmodifiableList(list) |
| Take top N | toList() | list.subList(0, Math.min(n, list.size())) |
| Map to DTOs | toList() | list.stream().map(toDTO).toList() |

How do you get the top 3 earners per city?

JAVA
Map<String, List<Employee>> top3PerCity = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getCity,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                list.sort(Comparator.comparingDouble(
                    Employee::getSalaryAsDouble).reversed());
                return list.subList(0, Math.min(3, list.size()));
            })));

Why use reversed() instead of negating the salary?

JAVA
// WRONG: negating double values to get descending order
// Breaks for Double.MIN_VALUE and positive zero vs negative zero
list.sort(Comparator.comparingDouble(
    e -> -e.getSalaryAsDouble()));  // fragile!

// CORRECT: use reversed() on the comparator
list.sort(Comparator.comparingDouble(
    Employee::getSalaryAsDouble).reversed());

reversed() is both clearer and correct for all number ranges, including edge cases like NaN and zero.

What are common mistakes?

Mistake: sorting the stream globally instead of per-group.

JAVA
// WRONG: sorts the entire stream, then groups.
// Each group is in stream order, not sorted by salary.
Map<String, List<Employee>> wrong = employees.stream()
    .sorted(Comparator.comparingDouble(
        Employee::getSalaryAsDouble).reversed())
    .collect(Collectors.groupingBy(Employee::getCity));
// The groups may appear sorted, but it depends on HashMap iteration order.

// CORRECT: sort inside each group with collectingAndThen
Map<String, List<Employee>> correct = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getCity,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                list.sort(Comparator.comparingDouble(
                    Employee::getSalaryAsDouble).reversed());
                return list;
            })));

Can you make the grouped lists immutable?

JAVA
Map<String, List<Employee>> immutable = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getCity,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                list.sort(Comparator.comparingDouble(
                    Employee::getSalaryAsDouble).reversed());
                return Collections.unmodifiableList(list);
            })));

What should a senior engineer know?

  • collectingAndThen is the general post-processing tool for any collected group.
  • Sorting inside the finisher keeps everything in one collect call and avoids a second pass.
  • For very large groups, a bounded priority queue (top-N) avoids fully sorting.
  • The finisher runs per group, so sort cost is proportional to the group size, not the total stream size.

What are common interview follow-up questions?

  • "How do you sort by salary ascending?" Remove the reversed() call.
  • "How do you sort by salary then by name?" Chain with thenComparing(Employee::getName).
  • "How do you get sorted department keys?" Use the three-argument groupingBy with TreeMap::new.

Visual Overview

DIAGRAM
  Stream<Employee>
  [Rajesh:Mumbai:75K, Priya:Mumbai:82K, Amit:Delhi:68K, Kavita:Delhi:91K, Neha:Delhi:78K]
       │
       │  groupingBy(city)
       ▼
  Groups (unsorted):
    Mumbai → [Rajesh:75K, Priya:82K]
    Delhi  → [Amit:68K, Kavita:91K, Neha:78K]
       │
       │  collectingAndThen: sort descending
       ▼
  Groups (sorted):
    Mumbai → [Priya:82K, Rajesh:75K]
    Delhi  → [Kavita:91K, Neha:78K, Amit:68K]

Key Takeaway

To group employees by city and sort each group by salary descending, use groupingBy with collectingAndThen as the downstream. collectingAndThen first collects each group into a list, then applies a finishing function that sorts the list with comparingDouble(salary).reversed().

collectingAndThen is the general tool for post-processing any collected group: sorting, trimming to top-N, making immutable, or mapping to DTOs. Sorting inside the finisher keeps everything in a single collect call.

At the National Bank of India, the regional leaderboard used this pattern to show employees bucketed by city with the highest paid at the top of each bucket. Use reversed() on the comparator rather than negating salary values, because reversed() is readable and correct for all number ranges.

Remember one sentence: groupingBy with collectingAndThen(toList(), sort-descending) groups by a key and orders each group in one collect call.

---

Share
Use ← → arrow keys to navigate