Card 05/ 09

WalkthroughDifficulty: Advanced2 min

Building a Comparator on Two Fields With One Reversed

By customer, then by amount largest first, and where reversed() has to go.

Comparator.comparing builds a comparator from one field, thenComparing adds a tie-breaker, and reversed() flips an order. Combining them sorts on several fields at once, with one catch: reversed() applies to everything before it in the chain.

A report wants invoices by customer, and within each customer by amount, largest first. Built up one piece at a time, it is four steps that each do one thing. You need an Invoice with customer() and pence() accessors. thenComparingLong and comparingLong are the variants for a long field, which compare the numbers directly instead of wrapping each one in an object first.

From one field to a two-field ordering with a reversal

Order by the first field

java
Comparator<Invoice> byCustomer = Comparator.comparing(Invoice::customer);

comparing takes a method that pulls out something already comparable and builds a comparator from it. Everything with the same customer now ties, and ties are where the next step goes.

Break the ties with the second field

java
Comparator<Invoice> byCustomerThenAmount =
        byCustomer.thenComparingLong(Invoice::pence);

thenComparing is consulted only when the one before it returns zero. The order of the chain is the order of precedence, reading left to right.

Reverse only the part that needs reversing

java
Comparator<Invoice> report =
        Comparator.comparing(Invoice::customer)
                  .thenComparing(Comparator.comparingLong(Invoice::pence).reversed());

This is the step that goes wrong. reversed() applies to the whole comparator it is called on, so adding it at the end of the chain would reverse the customer order too. To reverse only the amount, build that piece separately, reverse it, and hand it to thenComparing.

Decide where nulls go, if any are possible

java
Comparator<Invoice> safe =
        Comparator.comparing(Invoice::customer, Comparator.nullsLast(Comparator.naturalOrder()));

Without this, a null customer throws inside the sort rather than where the invoice was created. nullsFirst and nullsLast wrap an existing comparator and say where nulls go.

You know the comparator is right when the report shows customers A to Z, and each customer's largest invoice first. The step to remember is the third: reverse the small piece, never the whole chain that comes after it, whatever its length.