Java 8 introduced a few default methods and static factory methods on the Comparator interface using which developers can write Comparators in a declarative way. The Comparator interface combines the principles from Builder Pattern, Factory Method Pattern, Decorator Pattern and Functional Programming to come up with a very expressive API.
In this article I will be using a few examples to show how you can write Comparators using the newly defined methods. I will try to cover most of the use-cases that one would come across while writing Comparators.
The code samples can be found here.
What is a Comparator?
The Java documentation provides a good definition of a Comparator
.
A comparison function, which imposes a total ordering on some collection of objects. Comparators can be passed to a sort method (such as Collections.sort or Arrays.sort) to allow precise control over the sort order. Comparators can also be used to control the order of certain data structures (such as sorted sets or sorted maps), or to provide an ordering for collections of objects that don’t have a natural ordering.
Example domain object
For the purpose of demonstration I will be using the following domain object.
class Person {
private final String name;
private final int age;
Person(String name, int age) { ... }
String getName() { return name; }
int getAge() { return age; }
}
Use Cases
Let’s go through the below four use cases and see how we can write Comparators for each one of them
- A Comparator that sorts on a single property
- A concise version of comparing
- Writing efficient Comparators
- A Comparator that sorts on multiple properties
- Handling null values in a property
A Comparator that sorts on a single property
Let’s try to write a Comparator that would sort a collection of Person in the ascending order of a Person’s name.
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
...
Comparator<Person> comparator = comparing(Person::getName, naturalOrder());
The comparing
method is a static factory method on the Comparator
interface for creating Comparators. This method
takes in two arguments:
- keyExtractor - This is a function which gives the sort key. In the example above the keyExtractor is
Function<Person, String>
represented byPerson::getName
. - keyComparator - This is the Comparator which compares the sort key. In the example above the keyComparator is
Comparator<String>
represented bynaturalOrder()
.
Below are other variations of the example above.
// sort in descending order
Comparator<Person> comparator = comparing(Person::getName, reverseOrder());
// sort in case insensitive descending order
Comparator<Person> comparator =
comparing(Person::getName, String.CASE_INSENSITIVE_ORDER.reversed());
A concise version of comparing
There is an overloaded version of comparing
where only the keyExtractor has to be provided. If the key implements
the Comparable
interface then a naturalOrder()
is assumed for the keyComparator. When you use this overloaded version
the code will look like below:
Comparator<Person> comparator = comparing(Person::getName);
Writing efficient Comparators
Let’s try to write a Comparator that would sort a collection of Person in the ascending order of a Person’s age.
import static java.util.Comparator.comparingInt;
...
Comparator<Person> comparator = comparingInt(Person::getAge);
As the age property in Person is a primitive int, we can use the primitive specialization of the static
factory method comparing
. The costs associated with auto-boxing and unboxing are removed when the primitive
specializations are used. Here the keyExtractor, Person::getAge
, is a ToIntFunction<Person>
.
Similar to comparingInt
, the Comparator interface also has comparingDouble
and comparingLong
.
A Comparator that sorts on multiple properties
Let’s try to write a Comparator that would sort a collection of Person in the ascending order of a Person’s age and, within each age group, the Person objects should be sorted in ascending order of name.
Comparator<Person> comparator =
comparingInt(Person::getAge).thenComparing(Person::getName);
The thenComparing
is a default method on the Comparator interface that helps in composing multiple comparators. For
primitive properties the primitive specializations of the default method are thenComparingInt
, thenComparingLong
and thenComparingDouble
. These methods use the builder pattern to chain multiple Comparators.
Handling null values in a property
Let’s assume that the name property of Person can be null. Now let’s try to write a Comparator that would sort a collection of Person in the ascending order of a Person’s name. The question that would arise in your mind is that what should be sorting criteria for the null values of name? Let’s keep the Person objects, having null names, towards the end of the sorted collection.
import static java.util.Comparator.nullsLast;
...
Comparator<Person> comparing =
comparing(Person::getName, nullsLast(naturalOrder()));
nullsLast(naturalOrder())
sorts the non-null values is ascending order and keeps the null values at the end of a
sorted collection. Here nullsLast
is again a static factory method for creating a Comparators. It takes in a
Comparator as an argument which it decorates with null value handling functionality. Similarly
there is nullsFirst
, which considers null values ahead of non-null values.
Summary
Comparator in Java 8 has leveraged the new language features that were introduced in Java 8. By studying the source code of Comparator interface and using its APIs, I have gained insight into how I can write expressive code.