A New and Consistent Way of Implementing Order for Comparable Java Objects | Part 2


Introduction

In Part 1 of this topic you’ve seen different methods for implementing order for Java objects, showing you the transition from old Java versions to current versions. You’ve become acquainted with a new and consistent way of implementing order in “theory”. Now lets’s have a look at more realistic examples.

Sample Customer Class

The class all this blog post is about is a value class called Customer that represents a customer from an online shop. Of course, these customers need to be comparable because it’s pretty sure that they’ll be sorted somewhere somehow.

import java.util.*;
import java.time.*;

public final class Customer implements Comparable<Customer> {

    private String firstName;

    private String lastName;

    private LocalDate dateOfBirth;    /* May remain null. */

    private int orderCount;

    private double averageOrderValue;

    private boolean premiumCustomer;

    public Customer(String firstName, String lastName, LocalDate dateOfBirth,
                    int orderCount, double averageOrderValue, boolean premiumCustomer) {
        this.firstName = Objects.requireNonNull(firstName, "Parameter \"firstName\" is null.");
        this.lastName = Objects.requireNonNull(lastName, "Parameter \"lastName\" is null.");
        this.dateOfBirth = dateOfBirth;
        this.orderCount = orderCount;
        this.averageOrderValue = averageOrderValue;
        this.premiumCustomer = premiumCustomer;
    }

Of course, for demonstration purposes I tried to include as many different fields as possible. dateOfBirth is explicitly allowed to be null, in case the customer doesn’t want to reveal their date of birth (and the online shop isn’t dependend on it). orderCount is intended to count the orders of the customer, averageOrderValue should represent the average amount spent on all orders, and the Boolean premiumCustomer is set to true if for any reason this customer is regarded as a premium customer by the system (whose business logic is not important here). In real life, these three properties quite surely wouldn’t be represented directly as class fields, but rather be determined by some business logic every time they’re needed, but that’s a different story and shouldn’t distract from the idea of the examples shown in this blog post.

The constructor is self-explanatory. It is only important to mention that firstName and lastName are null-checked because they must not be null, whereas dateOfBirth may be null, as mentioned above.

The next code snippet includes the main function that initializes an array with 10 fake customers, then sorts them, and then prints out the sorted array in a formatted way. For demonstration purposes, two people have the same name (Ronald Hogan) and two people share the same date of birth (Richard Leblanc and Suzanne Murray on October 6, 1975).

    public static void main(String[] args) {

        /* Create and initialize customer array. */
        Customer[] customerArray = {
                new Customer("Richard", "Leblanc", LocalDate.of(1975, Month.OCTOBER, 6),
                             2, 498.30, false),
                new Customer("William", "Robinson", null,
                             2, 737.35, true),
                new Customer("Adella", "Wheeler", null,
                             7, 824.33, true),
                new Customer("Ronald", "Hogan", null,
                             2, 297.99, false),
                new Customer("Joseph", "Davis", LocalDate.of(1981, Month.NOVEMBER, 3),
                             9, 783.16, true),
                new Customer("Charles", "Quintana", LocalDate.of(1989, Month.JANUARY, 2),
                             4, 748.35, true),
                new Customer("Graham", "Reamer", LocalDate.of(1967, Month.MARCH, 16),
                             3, 255.96, false),
                new Customer("Suzanne", "Murray", LocalDate.of(1975, Month.OCTOBER, 6),
                             2, 199.49, false),
                new Customer("Ronald", "Hogan", LocalDate.of(1997, Month.NOVEMBER, 26),
                             2, 368.10, false),
                new Customer("Dolores", "Falco", null,
                             3, 556.41, true)
        };

        Arrays.sort(customerArray);

        /* Print customer array. */
        for (Customer customerAct : customerArray) {
            System.out.println(customerAct);
        }
    }

    @Override
    public String toString() {
        return String.format("Name: %-7s %-8s | DoB: %10s | Orders: %d | AOV: %3.2f | Premium? %5b",
                             firstName, lastName, dateOfBirth,
                             Integer.valueOf(orderCount),
                             Double.valueOf(averageOrderValue),
                             Boolean.valueOf(premiumCustomer));
    }

The Customer class is Comparable and thus implements the compareTo method. Since Java 8, the new way of implementing order is based on Comparators and actually makes the compareTo method simply “redirect itself” to such a Comparator. I’ve used this fact in order to implement seven different Comparators (discussed below). To not generate any compiler warnings about ununsed code, I’ve implemented a Comparator Switch as an enum that can be “switched” as shown in the marked line in the code snippet below. The compareTo method then simply looks at the switch and redirects to the chosen Comparator.

    private static enum ComparatorSwitch {
        LAST_NAME, FIRST_NAME, DATE_OF_BIRTH,
        ORDER_COUNT, ORDER_COUNT_REVERSED, PREMIUM_CUSTOMER,
        LAST_NAME_LENGTH
    }

    private static final ComparatorSwitch COMPARATOR_SWITCH
            = ComparatorSwitch.LAST_NAME;

    @Override
    public int compareTo(Customer otherCustomer) {
        switch (COMPARATOR_SWITCH) {
        case LAST_NAME:
            return LAST_NAME_COMPARATOR.compare(this, otherCustomer);
        case FIRST_NAME:
            return FIRST_NAME_COMPARATOR.compare(this, otherCustomer);
        case DATE_OF_BIRTH:
            return DATE_OF_BIRTH_COMPARATOR.compare(this, otherCustomer);
        case ORDER_COUNT:
            return ORDER_COUNT_COMPARATOR.compare(this, otherCustomer);
        case ORDER_COUNT_REVERSED:
            return ORDER_COUNT_REVERSED_COMPARATOR.compare(this, otherCustomer);
        case PREMIUM_CUSTOMER:
            return PREMIUM_CUSTOMER_COMPARATOR.compare(this, otherCustomer);
        case LAST_NAME_LENGTH:
            return LAST_NAME_LENGTH_COMPARATOR.compare(this, otherCustomer);
        default:
            assert false;
            return 0;
        }
    }
}

Sorting Customers by Last Name

Probably the most common case is to sort the customers alphabetically with respect to their last name. If two or more customers share the same last name, then their first names will be regarded as the “second most significant field”. If both last and first names are equal, then the next significant field, date of birth, will be taken into account. For the sake of brevity, no other fields are being examined afterwards.

    private static final Comparator<Customer> LAST_NAME_COMPARATOR
            = Comparator.comparing((Customer c) -> c.lastName)
            .thenComparing(c -> c.firstName)
            .thenComparing(c -> c.dateOfBirth, Comparator.nullsLast(LocalDate::compareTo));

Here is the program’s output:

Name: Joseph  Davis    | DoB: 1981-11-03 | Orders: 9 | AOV: 783.16 | Premium?  true
Name: Dolores Falco    | DoB:       null | Orders: 3 | AOV: 556.41 | Premium?  true
Name: Ronald  Hogan    | DoB: 1997-11-26 | Orders: 2 | AOV: 368.10 | Premium? false
Name: Ronald  Hogan    | DoB:       null | Orders: 2 | AOV: 297.99 | Premium? false
Name: Richard Leblanc  | DoB: 1975-10-06 | Orders: 2 | AOV: 498.30 | Premium? false
Name: Suzanne Murray   | DoB: 1975-10-06 | Orders: 2 | AOV: 199.49 | Premium? false
Name: Charles Quintana | DoB: 1989-01-02 | Orders: 4 | AOV: 748.35 | Premium?  true
Name: Graham  Reamer   | DoB: 1967-03-16 | Orders: 3 | AOV: 255.96 | Premium? false
Name: William Robinson | DoB:       null | Orders: 2 | AOV: 737.35 | Premium?  true
Name: Adella  Wheeler  | DoB:       null | Orders: 7 | AOV: 824.33 | Premium?  true

The two Ronald Hogans share the same name. In that case, date of birth tips the scales. However, one date of birth is null, but that’s not a problem at all. Comparator.nullsLast(LocalDate::compareTo) allows dealing with null values easily. It treats nulls as being last in order (in contrast to nullsFirst, which treats them as being first in order).

Sorting Customers by First Name

    private static final Comparator<Customer> FIRST_NAME_COMPARATOR
            = Comparator.comparing((Customer c) -> c.firstName)
            .thenComparing(c -> c.lastName)
            .thenComparing(c -> c.dateOfBirth, Comparator.nullsLast(LocalDate::compareTo));

Simply interchanging first name and last name creates a Comparator that sorts customers first based on their first name, then on their last name, and finally on their date of birth. Here is the sample output:

Name: Adella  Wheeler  | DoB:       null | Orders: 7 | AOV: 824.33 | Premium?  true
Name: Charles Quintana | DoB: 1989-01-02 | Orders: 4 | AOV: 748.35 | Premium?  true
Name: Dolores Falco    | DoB:       null | Orders: 3 | AOV: 556.41 | Premium?  true
Name: Graham  Reamer   | DoB: 1967-03-16 | Orders: 3 | AOV: 255.96 | Premium? false
Name: Joseph  Davis    | DoB: 1981-11-03 | Orders: 9 | AOV: 783.16 | Premium?  true
Name: Richard Leblanc  | DoB: 1975-10-06 | Orders: 2 | AOV: 498.30 | Premium? false
Name: Ronald  Hogan    | DoB: 1997-11-26 | Orders: 2 | AOV: 368.10 | Premium? false
Name: Ronald  Hogan    | DoB:       null | Orders: 2 | AOV: 297.99 | Premium? false
Name: Suzanne Murray   | DoB: 1975-10-06 | Orders: 2 | AOV: 199.49 | Premium? false
Name: William Robinson | DoB:       null | Orders: 2 | AOV: 737.35 | Premium?  true

Sorting Customers by Date of Birth

    private static final Comparator<Customer> DATE_OF_BIRTH_COMPARATOR
            = Comparator.comparing((Customer c) -> c.dateOfBirth,
                                   Comparator.nullsLast(LocalDate::compareTo))
            .thenComparing(c -> c.lastName)
            .thenComparing(c -> c.firstName);

Sorting customers by date of birth first isn’t magic either. Here is the output:

Name: Graham  Reamer   | DoB: 1967-03-16 | Orders: 3 | AOV: 255.96 | Premium? false
Name: Richard Leblanc  | DoB: 1975-10-06 | Orders: 2 | AOV: 498.30 | Premium? false
Name: Suzanne Murray   | DoB: 1975-10-06 | Orders: 2 | AOV: 199.49 | Premium? false
Name: Joseph  Davis    | DoB: 1981-11-03 | Orders: 9 | AOV: 783.16 | Premium?  true
Name: Charles Quintana | DoB: 1989-01-02 | Orders: 4 | AOV: 748.35 | Premium?  true
Name: Ronald  Hogan    | DoB: 1997-11-26 | Orders: 2 | AOV: 368.10 | Premium? false
Name: Dolores Falco    | DoB:       null | Orders: 3 | AOV: 556.41 | Premium?  true
Name: Ronald  Hogan    | DoB:       null | Orders: 2 | AOV: 297.99 | Premium? false
Name: William Robinson | DoB:       null | Orders: 2 | AOV: 737.35 | Premium?  true
Name: Adella  Wheeler  | DoB:       null | Orders: 7 | AOV: 824.33 | Premium?  true

You can clearly see how the null birthdays come last in order. If two people share the same birthday, as do Richard Leblanc and Suzanne Murray, they’re sorted by last name (and then by first name).

Sorting Customers by Order Count

    private static final Comparator<Customer> ORDER_COUNT_COMPARATOR
            = Comparator.comparingInt((Customer c) -> c.orderCount)
            .thenComparingDouble(c -> c.averageOrderValue);

It is recommended to use the primitive type versions [then]comparingXXX when comparing numbers. In this example, customers will be sorted with respect to their order count, which is an integer:

Name: Suzanne Murray   | DoB: 1975-10-06 | Orders: 2 | AOV: 199.49 | Premium? false
Name: Ronald  Hogan    | DoB:       null | Orders: 2 | AOV: 297.99 | Premium? false
Name: Ronald  Hogan    | DoB: 1997-11-26 | Orders: 2 | AOV: 368.10 | Premium? false
Name: Richard Leblanc  | DoB: 1975-10-06 | Orders: 2 | AOV: 498.30 | Premium? false
Name: William Robinson | DoB:       null | Orders: 2 | AOV: 737.35 | Premium?  true
Name: Graham  Reamer   | DoB: 1967-03-16 | Orders: 3 | AOV: 255.96 | Premium? false
Name: Dolores Falco    | DoB:       null | Orders: 3 | AOV: 556.41 | Premium?  true
Name: Charles Quintana | DoB: 1989-01-02 | Orders: 4 | AOV: 748.35 | Premium?  true
Name: Adella  Wheeler  | DoB:       null | Orders: 7 | AOV: 824.33 | Premium?  true
Name: Joseph  Davis    | DoB: 1981-11-03 | Orders: 9 | AOV: 783.16 | Premium?  true

This might probably not be the result the online shop is interested in. It would make more sense to have the customers with the largest number of orders first. Those customers should then be sorted by their average order value, again in descending order.

Sorting Customers by Reversed Order Count

The Comparator interface offers a default reversed() method that can be linked into the [then]comparing[XXX] call chain. However, don’t fall into the trap of assuming that this command might only refer to the last [then]comparing[XXX] method. No, reversed() reverses the whole Comparator up to and including all [then]comparing[XXX] methods before.

If you wrote .reversed() at the very end of the method chain shown above, the output is (by chance) correct. The last reversed method reverses both the order count and the average order value, which is exactly what we wanted. The same is true if you put .reversed() only after the order count comparingInt method (i.e., the middle line). Again, by chance the result is correct.

Please note that even though the output might be correct, writing code like this is deceiving. It relies on a fallacy whose implementation—by chance—makes it correct again. To put it crudely, it is hoping the “wrong and wrong” makes things right again. Such code is far from being self-explanatory.

A correct way of reversing single specific “intermediate Comparators” is shown here:

    private static final Comparator<Customer> ORDER_COUNT_REVERSED_COMPARATOR
            = Comparator.comparing((Customer c) -> Integer.valueOf(c.orderCount),
                                   Comparator.reverseOrder())
            .thenComparing((Customer c) -> Double.valueOf(c.averageOrderValue),
                           Comparator.reverseOrder());

It doesn’t look as elegant as the code snippets before. The implementation shown above uses the overloaded version thenComparing(Function, Comparator), which takes another Comparator in addition to the key extractor Function. The additional Comparator provided is Comparator.reverseOrder() (don’t confuse with Comparator.reversed()) which simply reverses the natural order of the data type in question.

Unfortunately, these overloaded methods do not exist for the primitive type versions thenComparingInt, thenComparingLong, and thenComparingDouble, so one has to use the general method that requires (auto) boxing and unboxing for primitive types. Since I personally want to be aware of any boxing and unboxing, I set up my IDE to warn me whenever auto boxing or auto unboxing occurs. I prefer doing all conversions manually, because it helps me looking out for pitfalls or performance traps that might otherwise be left unnoticed.

Another disadvantage is that one needs to “help” the compiler more often by providing the specific type (Customer) inside the lambda expressions.

Here is the sample output:

Name: Joseph  Davis    | DoB: 1981-11-03 | Orders: 9 | AOV: 783.16 | Premium?  true
Name: Adella  Wheeler  | DoB:       null | Orders: 7 | AOV: 824.33 | Premium?  true
Name: Charles Quintana | DoB: 1989-01-02 | Orders: 4 | AOV: 748.35 | Premium?  true
Name: Dolores Falco    | DoB:       null | Orders: 3 | AOV: 556.41 | Premium?  true
Name: Graham  Reamer   | DoB: 1967-03-16 | Orders: 3 | AOV: 255.96 | Premium? false
Name: William Robinson | DoB:       null | Orders: 2 | AOV: 737.35 | Premium?  true
Name: Richard Leblanc  | DoB: 1975-10-06 | Orders: 2 | AOV: 498.30 | Premium? false
Name: Ronald  Hogan    | DoB: 1997-11-26 | Orders: 2 | AOV: 368.10 | Premium? false
Name: Ronald  Hogan    | DoB:       null | Orders: 2 | AOV: 297.99 | Premium? false
Name: Suzanne Murray   | DoB: 1975-10-06 | Orders: 2 | AOV: 199.49 | Premium? false

Sorting Customers by Premium Customer

    private static final Comparator<Customer> PREMIUM_CUSTOMER_COMPARATOR
            = Comparator.comparing((Customer c) -> Boolean.valueOf(c.premiumCustomer),
                                   Comparator.reverseOrder())
            .thenComparing((Customer c) -> Integer.valueOf(c.orderCount),
                           Comparator.reverseOrder())
            .thenComparing((Customer c) -> Double.valueOf(c.averageOrderValue),
                           Comparator.reverseOrder());

Sorting customers with respect to premium customer (true should come before false), then order count (in descending order), and finally average order value (again in descending order) is shown above. All three ordering stages are explicitly reversed. They could be replaced by a single .reversed() at the very end of the [then]comparing chain, but please keep in mind the warning written in the last section. If you decide to do so, at least provide an explanatory comment.

The output of the program is:

Name: Joseph  Davis    | DoB: 1981-11-03 | Orders: 9 | AOV: 783.16 | Premium?  true
Name: Adella  Wheeler  | DoB:       null | Orders: 7 | AOV: 824.33 | Premium?  true
Name: Charles Quintana | DoB: 1989-01-02 | Orders: 4 | AOV: 748.35 | Premium?  true
Name: Dolores Falco    | DoB:       null | Orders: 3 | AOV: 556.41 | Premium?  true
Name: William Robinson | DoB:       null | Orders: 2 | AOV: 737.35 | Premium?  true
Name: Graham  Reamer   | DoB: 1967-03-16 | Orders: 3 | AOV: 255.96 | Premium? false
Name: Richard Leblanc  | DoB: 1975-10-06 | Orders: 2 | AOV: 498.30 | Premium? false
Name: Ronald  Hogan    | DoB: 1997-11-26 | Orders: 2 | AOV: 368.10 | Premium? false
Name: Ronald  Hogan    | DoB:       null | Orders: 2 | AOV: 297.99 | Premium? false
Name: Suzanne Murray   | DoB: 1975-10-06 | Orders: 2 | AOV: 199.49 | Premium? false

Note that the default (natural) order for Booleans is from false to true. Since we’ve reversed the order, the output correctly shows true before false.

Sorting Customers by Last Name Length

“Well, that’s all fine what you’ve shown us so far, but what if I don’t want to simply compare field values, but rather some more complicated things?”—No problem! See the final example where we sort customers according to the length of their last name:

    private static final Comparator<Customer> LAST_NAME_LENGTH_COMPARATOR
            = Comparator.comparingInt((Customer c) -> c.lastName.length())
            .thenComparing(c -> c.lastName)
            .thenComparing(c -> c.firstName);

This is the output:

Name: Joseph  Davis    | DoB: 1981-11-03 | Orders: 9 | AOV: 783.16 | Premium?  true
Name: Dolores Falco    | DoB:       null | Orders: 3 | AOV: 556.41 | Premium?  true
Name: Ronald  Hogan    | DoB:       null | Orders: 2 | AOV: 297.99 | Premium? false
Name: Ronald  Hogan    | DoB: 1997-11-26 | Orders: 2 | AOV: 368.10 | Premium? false
Name: Suzanne Murray   | DoB: 1975-10-06 | Orders: 2 | AOV: 199.49 | Premium? false
Name: Graham  Reamer   | DoB: 1967-03-16 | Orders: 3 | AOV: 255.96 | Premium? false
Name: Richard Leblanc  | DoB: 1975-10-06 | Orders: 2 | AOV: 498.30 | Premium? false
Name: Adella  Wheeler  | DoB:       null | Orders: 7 | AOV: 824.33 | Premium?  true
Name: Charles Quintana | DoB: 1989-01-02 | Orders: 4 | AOV: 748.35 | Premium?  true
Name: William Robinson | DoB:       null | Orders: 2 | AOV: 737.35 | Premium?  true

Simply provide the key extractor function that does whatever you want. You see that the way Comparators are written doesn’t change much.

Summary and Outlook

In this blog post, Part 2 of this topic, you’ve seen several examples of how comparing functionality can be implemented in Java. Although reversing single ordering steps is (still) a little bit cumbersome, the consistency and style of writing such comparison methods is impressive.

In the near future, I will keep my eyes open for a better solution when it comes to the reversed steps. I’m currently thinking of providing a utility class that simplifies reversing single comparison steps, as well as providing such methods for primitive types. Also, it’ll be interesting to find out if there is any performance penalty using such method chains. So far I’m pretty sure that any performance decrease (if there is any at all) won’t justify refraining from this new and consistent way of implementing order for comparable Java objects, except if one can show that the time spent in code that compares Java objects makes up a significant amount of the total runtime—which won’t be the case in the great majority of real-world applications.

Shortlink to this blog post: link.simplexacode.ch/gurm2019.01

Leave a Reply