Writing Comparators - The Java 8 Way

Datetime:2016-08-23 00:38:26          Topic: Java8           Share

Java 8 introduced a few default methods and static factory methods on the Comparator interface that allow developers to 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 by Person::getName .
  • keyComparator - This is the Comparator which compares the sort key. In the example above the keyComparator is Comparator<String> represented by naturalOrder() .

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.





About List