Back to Stream: Sorting & Comparator
Q2Question 2 of 23

Sort a stream by multiple fields: Comparator.comparing().thenComparing()

Key Concepts
  • Comparator.comparing(primary).thenComparing(secondary) chains multiple sort keys
  • thenComparing only triggers when the primary keys are equal
  • Use Comparator.reverseOrder() as the second argument for descending on a specific field
  • Calling .reversed() on the entire chain reverses ALL keys, not just the last one
  • nullsFirst() and nullsLast() wrap a comparator to handle null field values safely
  • sorted() is a stateful intermediate operation that buffers all elements before passing downstream

Definition

Chain Comparator.comparing(primaryKey).thenComparing(secondaryKey) for multi-field sorting, and use reverseOrder() for descending on a specific field.
  • Comparator.comparing(keyExtractor) creates a comparator based on a single field, such as comparing(Employee::department).
  • thenComparing(secondKey) adds a secondary sort criterion that applies only when the primary keys are equal.
  • reverseOrder() passed as the second argument reverses one specific field: thenComparing(salary, reverseOrder()) sorts salary descending while keeping the primary key ascending.
  • reversed() on the full chain reverses ALL keys, which is rarely what you want for mixed ascending/descending.
  • nullsFirst / nullsLast wraps any comparator to handle null values by placing them at the start or end.
  • You can chain unlimited keys: .comparing(dept).thenComparing(salary, reverseOrder()).thenComparing(name).

Sorting employees at National Bank of India is like organizing a card file. First sort by department name (primary). Within each department, sort by salary from highest to lowest (secondary descending). Within the same salary, sort alphabetically by name (tertiary ascending). Each thenComparing adds another level of tie-breaking.

Understanding the Concepts

What is multi-field sorting?

Multi-field sorting means ordering elements by a primary key, and when two elements have the same primary key, using a secondary key as a tiebreaker. The Java Stream API achieves this through Comparator.comparing and thenComparing.

How do you sort by name then by age?

JAVA
package com.bank.analytics;

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

public class MultiFieldSortDemo {
    record Employee(String id, String name, String department,
                    int age, BigDecimal salary) {}

    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("E001", "Rajesh",  "TRADING",    35, new BigDecimal("1800000")),
            new Employee("E002", "Priya",   "RISK",       28, new BigDecimal("1000000")),
            new Employee("E003", "Amit",    "OPERATIONS", 45, new BigDecimal("1500000")),
            new Employee("E004", "Amit",    "TRADING",    30, new BigDecimal("1200000")),
            new Employee("E005", "Suresh",  "RISK",       58, new BigDecimal("2000000")),
            new Employee("E006", "Priya",   "OPERATIONS", 32, new BigDecimal("1100000"))
        );

        // Sort by name, then by age
        List<Employee> sorted = employees.stream()
            .sorted(Comparator.comparing(Employee::name)
                .thenComparingInt(Employee::age))
            .toList();

        sorted.forEach(e ->
            System.out.println(e.name() + " (age " + e.age() + ")"));
    }
}
TEXT
Amit (age 30)
Amit (age 45)
Priya (age 28)
Priya (age 32)
Rajesh (age 35)
Suresh (age 58)

Both Amits and both Priyas are grouped together, with the younger one first because age is ascending.

How do you sort salary descending then name ascending?

Pass Comparator.reverseOrder() as the second argument to comparing for descending on that specific field.

JAVA
// Sort by salary descending, then by name ascending
List<Employee> bySalary = employees.stream()
    .sorted(Comparator.comparing(Employee::salary, Comparator.reverseOrder())
        .thenComparing(Employee::name))
    .toList();

bySalary.forEach(e ->
    System.out.println(e.name() + ": Rs." + e.salary()));
TEXT
Suresh: Rs.2000000
Rajesh: Rs.1800000
Amit: Rs.1500000
Amit: Rs.1200000
Meena: Rs.1100000
Priya: Rs.1000000

How do you sort by department, salary descending, then name?

JAVA
// Three-level sort: department asc, salary desc, name asc
List<Employee> tripleSort = employees.stream()
    .sorted(Comparator.comparing(Employee::department)
        .thenComparing(Employee::salary, Comparator.reverseOrder())
        .thenComparing(Employee::name))
    .toList();

tripleSort.forEach(e ->
    System.out.printf("%-12s %-8s Rs.%s%n", e.department(), e.name(), e.salary()));

What is the difference between reversed() and reverseOrder()?

| Method | Scope | Effect |
|--------|-------|--------|
| .reversed() on the chain | Reverses the ENTIRE comparator chain | ALL keys flip direction |
| reverseOrder() as second argument | Reverses ONE specific key | Only that key is descending |

WRONG: Using reversed() when you want only one key descending.

JAVA
// WRONG: reversed() flips ALL keys
Comparator<Employee> wrong = Comparator.comparing(Employee::department)
    .thenComparing(Employee::salary)
    .reversed();  // department is now DESC too!

CORRECT: Use reverseOrder() on the specific key.

JAVA
// CORRECT: only salary is descending
Comparator<Employee> correct = Comparator.comparing(Employee::department)
    .thenComparing(Employee::salary, Comparator.reverseOrder());

How do you handle null values in sorting?

JAVA
// Null-safe sorting: nulls go to the end
List<Employee> nullSafe = employees.stream()
    .sorted(Comparator.comparing(Employee::department,
        Comparator.nullsLast(Comparator.naturalOrder())))
    .toList();

What about comparingInt vs comparing for primitive fields?

Use comparingInt, comparingLong, and comparingDouble for primitive fields to avoid autoboxing overhead.

JAVA
// comparing(Employee::age) boxes int to Integer for each comparison
// comparingInt(Employee::age) uses primitive int directly
Comparator<Employee> efficient = Comparator.comparingInt(Employee::age);

What should a senior engineer know?

  • sorted() is a stateful intermediate operation. It buffers ALL elements into memory before passing them downstream. For very large streams, this can cause OutOfMemoryError.
  • Java's sort algorithm (TimSort) is stable: equal elements maintain their original relative order. This means thenComparing produces deterministic results.
  • For complex comparators, extract them into a static field or method for readability and reuse.
  • In parallel streams, sorting requires merging partial sorted results, which adds overhead. Sorting is often a pipeline bottleneck.

Visual Overview

DIAGRAM
  employees stream
       │
       ▼
  sorted(comparing(department)
         .thenComparing(salary, reverseOrder())
         .thenComparing(name))
       │
       ▼
  ┌───────────────────────────────────────┐
  │ 1. Compare department (ASC)           │
  │    OPERATIONS < RISK < TRADING        │
  │                                       │
  │ 2. If departments equal:              │
  │    Compare salary (DESC)              │
  │    2000000 > 1500000 > 1000000        │
  │                                       │
  │ 3. If salaries also equal:            │
  │    Compare name (ASC)                 │
  │    Amit < Priya < Rajesh              │
  └───────────────────────────────────────┘

Key Takeaway

Multi-field sorting uses Comparator.comparing(primaryKey).thenComparing(secondaryKey). The thenComparing method is a default method on Comparator that adds a tiebreaker: it is only invoked when the primary keys are equal. For descending order on one specific field, pass Comparator.reverseOrder() as the second argument. Calling .reversed() on the entire chain reverses ALL keys, not just the last one, which is a common mistake.

At National Bank of India, the trade blotter sorts transactions by department ascending, then salary descending, then name ascending. This replaces a 30-line custom Comparator with nested if-else blocks. For null-safe sorting, wrap the inner comparator with nullsFirst() or nullsLast().

The sorted() operation buffers all elements in memory because it needs to see everything before producing output. For large datasets, consider whether you truly need a full sort or whether a partial sort (top-N with sorted().limit(n)) is sufficient.

Remember one sentence: thenComparing adds a tiebreaker, and reverseOrder() on one key is different from reversed() on the whole chain.

---

Share
Use ← → arrow keys to navigate