Back to Stream: Sorting & Comparator
Q8Question 8 of 23

Sort employees by multiple keys with tie-breaking using Comparator.comparing and thenComparing

Key Concepts
  • `Comparator.comparing(keyExtractor)` builds a comparator from a sort key using the key's natural order.
  • `thenComparing` chains secondary keys that are consulted only when all earlier keys report equal.
  • `reversed()` flips the direction of one comparator stage; reverse only the stage you want descending, not the whole chain.
  • `comparingInt`, `comparingLong`, and `comparingDouble` avoid boxing for primitive keys.
  • `Comparator.nullsFirst` and `nullsLast` wrap a key comparator so null keys sort to a defined end instead of throwing.
  • Java's sort is stable, so elements equal under every comparator keep their original order.
  • Composing comparators expresses a multi-key sort in one expression without writing a manual `compare` method.

Definition

`comparing(key).thenComparing(nextKey)...` layers sort keys, each breaking ties left by the previous, with `reversed` for a descending key.

Comparators compose from small parts into a multi-key sort.

  • Comparator.comparing(keyExtractor) – builds the primary comparator from a sort key such as Employee::getDepartment.
  • thenComparing(nextKey) – adds a secondary key consulted only when all earlier keys tie.
  • reversed() – flips a single stage to descending; applied to the whole chain it flips everything.
  • comparingInt/comparingLong/comparingDouble – primitive-specialized key extractors that avoid boxing.
  • nullsFirst/nullsLast – wrap a key comparator so null keys sort to a defined end.

Priya arranges employee cards first by department. Within the same department she puts the highest earners on top. If two people in a department earn exactly the same, she breaks the tie alphabetically by name. Each rule only matters when every rule before it is equal, which is precisely how thenComparing layers the keys.

Understanding the Concepts

What is Comparator.comparing?

Comparator.comparing(keyExtractor) takes a function that pulls a sort key out of each object and returns a comparator that orders objects by that key's natural order. It replaces hand-written compare methods for the common case of sorting by a field.

JAVA
package com.bank.sort;

import com.bank.model.Employee;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class ComparingDemo {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
                new Employee(1, "Rajesh", "Trading", new BigDecimal("900000")),
                new Employee(2, "Priya", "Risk", new BigDecimal("850000"))
        );
        List<Employee> byDept = employees.stream()
                .sorted(Comparator.comparing(Employee::department))
                .collect(Collectors.toList());
        byDept.forEach(e -> System.out.println(e.department()));
    }
}
TEXT
Risk
Trading

The Employee record:

JAVA
package com.bank.model;

import java.math.BigDecimal;

public record Employee(long id, String name, String department, BigDecimal salary) { }

How does thenComparing chain secondary keys?

thenComparing adds a tie-breaker that the comparator consults only when all earlier comparators report the two elements equal. Chaining several produces a layered sort.

JAVA
// Sort by department ascending, then by name ascending within a department
Comparator<Employee> byDeptThenName = Comparator
        .comparing(Employee::department)
        .thenComparing(Employee::name);
JAVA
// Within Trading, two employees order by name only because the departments tie
List<Employee> sorted = employees.stream()
        .sorted(byDeptThenName)
        .collect(java.util.stream.Collectors.toList());

How do you make one key descending?

To make a single key descending, call reversed() on that specific comparator stage, or pass a reverse-order comparator to the key-based overload. Reversing only the stage keeps the other keys in their intended direction.

JAVA
// department ascending, salary DESCENDING, name ascending
Comparator<Employee> cmp = Comparator
        .comparing(Employee::department)
        .thenComparing(Comparator.comparing(Employee::salary).reversed())
        .thenComparing(Employee::name);
JAVA
package com.bank.sort;

import com.bank.model.Employee;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class MultiKeySort {
    public List<Employee> sort(List<Employee> employees) {
        Comparator<Employee> cmp = Comparator
                .comparing(Employee::department)
                .thenComparing(Comparator.comparing(Employee::salary).reversed())
                .thenComparing(Employee::name);
        return employees.stream().sorted(cmp).collect(Collectors.toList());
    }
}
TEXT
// Within each department, highest salary first; equal salaries by name
Risk      Priya    850000
Trading   Rajesh  1200000
Trading   Pankaj   990000

Why reverse only the stage and not the whole chain?

Calling reversed() on the fully composed comparator flips every key at once, which is almost never what a multi-key sort wants. You usually want some keys ascending and one descending, so you reverse the one stage.

JAVA
// WRONG: reversed() on the whole chain flips department AND name too
Comparator<Employee> wrong = Comparator
        .comparing(Employee::department)
        .thenComparing(Employee::salary)
        .thenComparing(Employee::name)
        .reversed(); // now department is descending, name is descending
JAVA
// CORRECT: reverse only the salary stage
Comparator<Employee> right = Comparator
        .comparing(Employee::department)
        .thenComparing(Comparator.comparing(Employee::salary).reversed())
        .thenComparing(Employee::name);

How do you avoid boxing with primitive keys?

For int, long, and double keys, use comparingInt, comparingLong, and comparingDouble. These return specialized comparators that read the primitive directly, avoiding the autoboxing that comparing would do.

DIAGRAM
| Key type | Use | Why |
| --- | --- | --- |
| `int` | `comparingInt(Employee::getAge)` | no `Integer` boxing |
| `long` | `comparingLong(Employee::getId)` | no `Long` boxing |
| `double` | `comparingDouble(e -> e.salary().doubleValue())` | no `Double` boxing |
| object | `comparing(Employee::getName)` | natural `Comparable` order |
JAVA
// Primitive-specialized, no boxing, and a reversed stage for descending salary
Comparator<Employee> cmp = Comparator
        .comparing(Employee::department)
        .thenComparing(Comparator.comparingDouble((Employee e) -> e.salary().doubleValue()).reversed())
        .thenComparing(Employee::name);

How do you handle null keys safely?

If a key may be null, a plain comparator throws NullPointerException. Wrap the key comparator with Comparator.nullsFirst or nullsLast so nulls sort to a defined end.

JAVA
// WRONG: throws NPE when a department is null
Comparator<Employee> unsafe = Comparator.comparing(Employee::department);
JAVA
// CORRECT: nulls sort last, present values by natural order
Comparator<Employee> safe = Comparator.comparing(
        Employee::department,
        Comparator.nullsLast(Comparator.naturalOrder()));
TEXT
// Employees with a null department land at the end of the sorted list

How does sort stability interact with the comparator?

Java's sort (and Stream.sorted) is stable: elements that the comparator reports equal keep their original relative order. This means your final tie-breaker only matters when you want a specific order among otherwise-equal elements; without it, equal elements stay in input order.

JAVA
// Without a name tie-breaker, two equal-salary Trading employees keep input order
Comparator<Employee> noTieBreak = Comparator
        .comparing(Employee::department)
        .thenComparing(Comparator.comparing(Employee::salary).reversed());
// stable sort preserves their original order among equals

What are common mistakes?

Beyond reversing the whole chain, a common mistake is comparing money with comparingDouble on BigDecimal.doubleValue(), which loses precision for large or fractional amounts. Compare BigDecimal directly instead.

JAVA
// WRONG: doubleValue() can lose precision for money
Comparator.comparingDouble((Employee e) -> e.salary().doubleValue());
JAVA
// CORRECT: compare BigDecimal by its natural order
Comparator.comparing(Employee::salary);

Another mistake is sorting the original list with list.sort(...) when the caller expected the input untouched. Prefer stream().sorted(...) to produce a new list, or copy before sorting in place.

JAVA
// Produces a new sorted list, leaves the input unchanged
List<Employee> sorted = employees.stream().sorted(cmp).collect(Collectors.toList());

When should you compose comparators, and what are the follow-ups?

Compose comparators whenever a sort needs more than one key, which is the norm for reports and exports: a staff directory listed by department alphabetically, highest earners first within a department, and names breaking remaining ties. Interviewers follow up by asking how to make one key descending (reverse that stage), how to handle nulls (nullsFirst/nullsLast), why primitive specializations exist (avoid boxing), and whether the sort is stable (yes, so equal elements keep input order).

Visual Overview

DIAGRAM
  comparator composition: three layered keys

  compare two employees
        │
        ▼
  ┌───────────────────────────┐
  │ department (ascending)     │── not equal ──► use this result
  └─────────────┬─────────────┘
                │ equal
                ▼
  ┌───────────────────────────┐
  │ salary.reversed() (desc)   │── not equal ──► use this result
  └─────────────┬─────────────┘
                │ equal
                ▼
  ┌───────────────────────────┐
  │ name (ascending)           │── not equal ──► use this result
  └─────────────┬─────────────┘
                │ equal
                ▼
        stable sort keeps original input order
TEXT
  reverse the STAGE, not the CHAIN

  comparing(dept)                       comparing(dept)
    .thenComparing(salary)                .thenComparing(salary.reversed())
    .thenComparing(name)                  .thenComparing(name)
    .reversed()        <-- WRONG          <-- CORRECT
  flips dept, salary, AND name          flips ONLY salary
TEXT
  null handling

  comparing(dept)                         comparing(dept, nullsLast(natural))
     null -> NullPointerException            null -> sorts to the END, safe

Key Takeaway

To sort by several keys I compose comparators. Comparator.comparing builds the primary comparator from a key extractor, thenComparing adds secondary keys that are consulted only when all earlier keys tie, and I flip a single key's direction with reversed. So for department ascending, then salary descending, then name ascending, I write comparing on department, thenComparing on the salary comparator reversed, and thenComparing on name as the final tie-breaker.

In my banking work at the National Bank of India the staff directory export had to list departments alphabetically, highest earners first within each department, and break remaining salary ties alphabetically by name so the export was stable and deterministic. One composed comparator expressed all three rules, and because Java's sort is stable, employees equal under every key kept their original order.

The senior details I always mention are three. First, I reverse only the specific stage I want descending, because calling reversed on the whole chain flips every key, which is rarely intended. Second, for nullable keys I wrap the key comparator with nullsFirst or nullsLast so nulls sort to a defined end instead of throwing a NullPointerException. Third, for primitive keys I use comparingInt, comparingLong, or comparingDouble to avoid boxing, though for money I compare BigDecimal directly rather than through doubleValue, which would lose precision.

Think of Priya laying out employee cards: department first, highest earners on top within a department, and names settling the last ties, each rule mattering only when the ones before it are equal.

Remember one sentence: comparing(key).thenComparing(nextKey) layers sort keys, and you reverse only the single stage you want descending.

---

Share
Use ← → arrow keys to navigate