Definition
Comparators compose from small parts into a multi-key sort.
Comparator.comparing(keyExtractor)– builds the primary comparator from a sort key such asEmployee::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.
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()));
}
}
Risk
Trading
The Employee record:
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.
// Sort by department ascending, then by name ascending within a department
Comparator<Employee> byDeptThenName = Comparator
.comparing(Employee::department)
.thenComparing(Employee::name);
// 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.
// department ascending, salary DESCENDING, name ascending
Comparator<Employee> cmp = Comparator
.comparing(Employee::department)
.thenComparing(Comparator.comparing(Employee::salary).reversed())
.thenComparing(Employee::name);
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());
}
}
// 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.
// 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
// 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.
| 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 |
// 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.
// WRONG: throws NPE when a department is null
Comparator<Employee> unsafe = Comparator.comparing(Employee::department);
// CORRECT: nulls sort last, present values by natural order
Comparator<Employee> safe = Comparator.comparing(
Employee::department,
Comparator.nullsLast(Comparator.naturalOrder()));
// 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.
// 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.
// WRONG: doubleValue() can lose precision for money
Comparator.comparingDouble((Employee e) -> e.salary().doubleValue());
// 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.
// 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
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
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
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.
---