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 Comparator
s 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 null
s 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 Comparator
s” 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