Back to Stream: Sorting & Comparator
Q4Question 4 of 23

Comparator.comparing + thenComparing + reversed + nullsFirst/nullsLast: complex sort orders

Key Concepts
  • Comparator.comparing(keyExtractor) creates a comparator from a single field
  • thenComparing adds secondary sort keys that trigger only when the primary keys are equal
  • Comparator.reverseOrder() as the second argument reverses a specific key; .reversed() at the end reverses the entire chain
  • nullsFirst and nullsLast wrap a comparator to handle null values without NullPointerException
  • comparingInt/Long/Double avoids autoboxing when extracting primitive fields
  • Fluent chain replaces 25 lines of manual compare() with 3 readable lines
  • The .reversed() pitfall: calling it at the end reverses ALL keys, not just the last one

Definition

Java 8's Comparator fluent API builds complex sort orders declaratively with comparing, thenComparing, reversed, and nullsFirst/nullsLast, replacing verbose manual compare methods.
  • 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?

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

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

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

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

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

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

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

---

Share
Use ← → arrow keys to navigate