Definition
- comparing(keyExtractor) – creates a comparator from a field:
Comparator.comparing(Trade::getSymbol). - thenComparing(keyExtractor) – adds a secondary sort key, used only when the primary keys are equal.
- reversed() – reverses the ENTIRE comparator chain; to reverse only one key, pass
Comparator.reverseOrder()as the second argument. - nullsFirst/nullsLast – wraps a comparator to handle null values by placing them first or last.
- comparingInt/Long/Double – avoids autoboxing for primitive fields.
- Advantage – replaces 25 lines of manual
compare()with nested null checks and if-else with 3 readable lines.
The head trader needs trades sorted by department alphabetically (null departments last), then by amount descending within each department, then by trade ID for tiebreaks. Manual compare: 25 lines of nested if-else with null checks. Comparator chain: three readable lines.
Understanding the Concepts
How do you build complex sort orders?
The Comparator fluent API builds multi-field sort orders declaratively.
How does comparing work?
package com.bank.service;
import java.util.Comparator;
import java.math.BigDecimal;
public class TradeSorter {
// Single field: ascending by symbol
Comparator<Trade> bySymbol = Comparator.comparing(Trade::getSymbol);
// HDFC, INFY, RELIANCE, TCS, WIPRO
// Descending: reversed()
Comparator<Trade> bySymbolDesc = Comparator.comparing(Trade::getSymbol).reversed();
// WIPRO, TCS, RELIANCE, INFY, HDFC
}
How does thenComparing work?
// Primary: symbol ascending. Secondary: amount descending.
Comparator<Trade> multiKey = Comparator.comparing(Trade::getSymbol)
.thenComparing(Trade::getAmount, Comparator.reverseOrder());
// Within RELIANCE: 245050 before 123750 (descending)
What is the reversed() pitfall?
// WRONG: reversed() at end reverses ALL keys
Comparator<Trade> wrong = Comparator.comparing(Trade::getSymbol)
.thenComparing(Trade::getAmount)
.reversed();
// Symbol DESCENDING AND amount DESCENDING!
// CORRECT: reverse only amount
Comparator<Trade> right = Comparator.comparing(Trade::getSymbol)
.thenComparing(Trade::getAmount, Comparator.reverseOrder());
// Symbol ASCENDING, amount DESCENDING within each symbol
How do nullsFirst and nullsLast work?
// Department might be null for some trades
Comparator<Trade> nullSafe = Comparator.comparing(Trade::getDepartment,
Comparator.nullsLast(Comparator.naturalOrder()));
// Non-null departments sorted alphabetically, null departments at the END
Comparator<Trade> nullFirst = Comparator.comparing(Trade::getDepartment,
Comparator.nullsFirst(Comparator.naturalOrder()));
// Null departments at the BEGINNING, then alphabetical
How does comparingInt avoid boxing?
// Boxing: comparing extracts Integer wrapper object
Comparator<Trade> boxing = Comparator.comparing(Trade::getQuantity);
// No boxing: comparingInt extracts primitive int
Comparator<Trade> noBoxing = Comparator.comparingInt(Trade::getQuantity);
How does the fluent chain compare to manual code?
// WRONG: manual compare (25 lines, error-prone)
class TradeComparator implements Comparator<Trade> {
public int compare(Trade a, Trade b) {
if (a.getDepartment() == null && b.getDepartment() == null) {
// fall through to secondary
} else if (a.getDepartment() == null) {
return 1; // null last
} else if (b.getDepartment() == null) {
return -1;
} else {
int deptCmp = a.getDepartment().compareTo(b.getDepartment());
if (deptCmp != 0) return deptCmp;
}
int amtCmp = b.getAmount().compareTo(a.getAmount());
if (amtCmp != 0) return amtCmp;
return a.getTradeId().compareTo(b.getTradeId());
}
}
// CORRECT: fluent chain (3 lines, readable)
Comparator<Trade> fluent = Comparator.comparing(Trade::getDepartment,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(Trade::getAmount, Comparator.reverseOrder())
.thenComparing(Trade::getTradeId);
What should a senior engineer know?
- thenComparing only evaluates when the previous comparator returns 0, making it efficient for high-cardinality primary keys.
- sorted() in streams is a stateful intermediate operation that buffers all elements before passing them downstream.
- For stable sorts (preserving order of equal elements), the fluent API is stable by default.
- Comparator.comparing with a Comparator argument is needed for nullsFirst/nullsLast and reverseOrder.
What are common interview follow-up questions?
- Why use comparingInt instead of comparing? (Avoids autoboxing overhead for primitive fields.)
- Can you combine nullsFirst with reversed? (Yes:
comparing(field, nullsFirst(reverseOrder()))puts nulls first, non-nulls in descending order.) - Does sorted() preserve insertion order for equal elements? (Yes, it is a stable sort.)
Visual Overview
Comparator Chain Evaluation:
Trade A: dept="Equity", amount=245050, id="TRD-001"
Trade B: dept="Equity", amount=123750, id="TRD-002"
Step 1: comparing(dept, nullsLast(natural))
"Equity".compareTo("Equity") = 0 → TIE → next key
Step 2: thenComparing(amount, reverseOrder())
245050 vs 123750 → 245050 > 123750 → A before B
Result: A comes first (higher amount within same department)
Key Takeaway
Java 8's Comparator fluent API builds complex sort orders declaratively. Comparator.comparing(keyExtractor) creates a comparator from a single field. thenComparing adds secondary sort keys that trigger only when the primary key is equal. Comparator.reverseOrder() as the second argument reverses a specific key, while .reversed() at the end reverses the entire chain.
nullsFirst and nullsLast wrap a comparator to handle null values without NullPointerException. comparingInt, comparingLong, and comparingDouble avoid autoboxing for primitive fields. The fluent chain replaces 25 lines of manual compare() with 3 readable lines.
The key pitfall is .reversed() at the end of the chain: it reverses ALL keys, not just the last one. To reverse only one specific key, pass Comparator.reverseOrder() as the second argument to comparing or thenComparing for that key.
Remember one sentence: build complex sort orders with comparing().thenComparing().reversed(), using reverseOrder() for specific keys and nullsLast for null safety.
---