Sorting with Comparator

Sort lists by one or more fields with Comparator.comparing, thenComparing, reversed and nullsLast, and map API sort parameters to safe comparators.

Syntaxproducts.sort(Comparator.comparing(Product::category) .thenComparing(Product::price, Comparator.reverseOrder())); users.sort(Comparator.comparing(User::lastLogin, Comparator.nullsLast(Comparator.reverseOrder())));

A Comparator<T> decides the order of two values: compare(a, b) returns a negative number when a comes first, zero when they tie and a positive number when b comes first. Classes such as String, Integer and LocalDate also have a natural order through Comparable. A comparator lets you sort by anything else without changing the class.

Building comparators

  • Comparator.comparing(Product::name) sorts by a key that is itself comparable.
  • comparingInt, comparingLong and comparingDouble avoid boxing for numeric keys.
  • .thenComparing(...) breaks ties with a second key.
  • .reversed() flips the whole comparator built so far; Comparator.reverseOrder() passed as the second argument of comparing or thenComparing flips one key only.
  • Comparator.nullsFirst(...) and nullsLast(...) wrap a comparator so null values do not throw.

Sort in place with list.sort(comparator), or produce a new list with stream.sorted(comparator).toList(). Calling sort on a List.of list throws UnsupportedOperationException. The sort is stable: elements that compare equal keep their previous relative order.

Mistakes people make

  • Subtraction comparators. (a, b) -> a - b overflows for large values of opposite sign and returns the wrong sign. Use Integer.compare or comparingInt.
  • Reversing too much. comparing(Order::status).thenComparing(Order::total).reversed() reverses both keys, not only the total.
  • Lambdas in a chain. Comparator.comparing(p -> p.name()).reversed() does not compile, because the lambda's parameter is inferred as Object before reversed() is applied. Use a method reference or write the type: (Product p) -> p.name().
  • Plain string order. String comparison uses UTF-16 character values, so "Zoe" sorts before "adam". Use String.CASE_INSENSITIVE_ORDER, or a java.text.Collator for language-aware order.

Back-end angle

APIs often accept ?sort=-createdAt,name. Map each allowed field name to a comparator, reject anything else with a 400, and combine them with thenComparing. Never let the raw parameter pick a field by reflection or reach SQL unchecked. For data that lives in a database and is paginated, sort there with ORDER BY and an index: sorting in memory only orders the rows you already loaded, so page two would be wrong.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Order search results by relevance and then by newest first before returning a small in-memory result set.
  • Translate a ?sort=-createdAt,name query parameter into a comparator chain and reject unknown fields with 400.
  • List customers by last login with never-logged-in accounts at the end of an admin report.

More examples

Why subtraction comparators break

1,500,000,000 minus -2,000,000,000 is larger than Integer.MAX_VALUE, wraps to a negative number and claims the wrong order. Integer.compare never overflows.

Example · java
void main() {
    List<Integer> values = new ArrayList<>(List.of(-2_000_000_000, 1_500_000_000, 0));

    values.sort((a, b) -> a - b);         // overflows for large values of opposite sign
    IO.println("subtraction:     " + values);

    values.sort(Integer::compare);
    IO.println("Integer.compare: " + values);
}
// Output:
// subtraction:     [0, 1500000000, -2000000000]
// Integer.compare: [-2000000000, 0, 1500000000]

Newest login first, never-logged-in last

Without nullsLast the comparator would throw a NullPointerException on linus. reverseOrder inside nullsLast flips the dates but keeps the nulls at the end.

Example · java
record Customer(String name, LocalDate lastLogin) {}

void main() {
    List<Customer> customers = new ArrayList<>(List.of(
            new Customer("ada", LocalDate.of(2026, 3, 1)),
            new Customer("linus", null),
            new Customer("grace", LocalDate.of(2026, 5, 20)),
            new Customer("ken", LocalDate.of(2026, 1, 15))));

    customers.sort(Comparator.comparing(Customer::lastLogin,
            Comparator.nullsLast(Comparator.reverseOrder())));
    for (Customer c : customers) {
        IO.println(c.name() + " " + c.lastLogin());
    }

    customers.sort(Comparator.comparing(Customer::name).reversed());
    IO.println(customers.stream().map(Customer::name).toList());
}
// Output:
// grace 2026-05-20
// ada 2026-03-01
// ken 2026-01-15
// linus null
// [linus, ken, grace, ada]

Turning ?sort= into a comparator

Only the fields in the allow list can be sorted on, and each one is reversed on its own before being chained. List.of cannot be sorted in place, so the stream produces a new sorted list.

Example · java
record Order(String id, int totalCents, LocalDate placed) {}

static final Map<String, Comparator<Order>> SORTABLE = Map.of(
        "total", Comparator.comparingInt(Order::totalCents),
        "placed", Comparator.comparing(Order::placed));

Comparator<Order> parseSort(String param) {
    Comparator<Order> result = null;
    for (String part : param.split(",")) {
        boolean descending = part.startsWith("-");
        String field = descending ? part.substring(1) : part;
        Comparator<Order> next = SORTABLE.get(field);
        if (next == null) {
            throw new IllegalArgumentException("cannot sort by " + field);
        }
        if (descending) {
            next = next.reversed();
        }
        result = result == null ? next : result.thenComparing(next);
    }
    return result;
}

void main() {
    List<Order> orders = List.of(
            new Order("o-1", 2500, LocalDate.of(2026, 3, 2)),
            new Order("o-2", 9900, LocalDate.of(2026, 3, 1)),
            new Order("o-3", 2500, LocalDate.of(2026, 3, 5)));

    IO.println("-total,placed: " + orders.stream().sorted(parseSort("-total,placed")).map(Order::id).toList());
    IO.println("-placed: " + orders.stream().sorted(parseSort("-placed")).map(Order::id).toList());
    try {
        parseSort("passwordHash");
    } catch (IllegalArgumentException e) {
        IO.println("400 Bad Request: " + e.getMessage());
    }
}
// Output:
// -total,placed: [o-2, o-1, o-3]
// -placed: [o-3, o-1, o-2]
// 400 Bad Request: cannot sort by passwordHash

Discussion

  • Be the first to comment on this lesson.