Comparator nullsFirst and nullsLast

Introduction

A Comparator is a comparison function which imposes a total ordering on a collection of objects. We pass a Comparator to a sort method (such as Collections.sort or Arrays.sort) and it allows us to sort a collection of objects by imposing a certain ordering. They are also used to control the ordering of certain data structures like SortedSet or a SortedMap (like TreeSet or TreeMap). The Comparator class has many static utility methods that we can use to build Comparator instances easily. I already wrote about Comparator’s comparing methods. Now we will learn about the Comparator nullsFirst and nullsLast methods.

Comparator.compare method

There are many possible places where we would use a Comparator. We might use it for sorting a list or an array, pass a Comparator to a TreeSet or a TreeMap, or sort a stream of objects using a Comparator. When there are one or more null elements in the collection, we would get a NullPointerException.

Remember that a Comparator is a functional interface with a single abstract method called compareIt accepts two elements and returns

  • 0 if they are equal.
  • a negative number if first argument is lesser than the second.
  • a positive number if first argument is greater than the second.

Refer to the Sorting using a Comparator section to understand how a Comparator works.

Problem of nulls when comparing

Let us say we have a list of fruit names as a list. We call the sort method on the list and pass a comparator that is returned by Comparator.naturalOrder(). Thus it sorts the list using a comparator that orders the elements (Strings) by their natural ordering. The output is the fruit names sorted lexicographically as shown below.

List<String> fruits = Arrays.asList("pear", "apple", "grapes","orange");
fruits.sort(Comparator.naturalOrder());
System.out.println(fruits); //[apple, grapes, orange, pear]

The problem arises when one or more nulls are present in the list.

fruits = Arrays.asList("pear", "apple", "grapes", null, "orange");
fruits.sort(Comparator.naturalOrder());

Running the above code fails with a java.lang.NullPointerException. We thus need a way to handle nulls with Comparators. i.e., when one of the arguments passed to the comparator is null. Also, the Comparators returned by the static methods do not handle nulls (say when a key mapper maps an element to a null when using the Comparator#comparing method). We will explore this in the post.

Comparator – Dealing with null values

The Comparator class has two methods that help with dealing with nulls. They are Comparator.nullsFirst and Comparator.nullsLastWhich one to use depends on how you want to sort the collection.

  • If you want the nulls to be ordered before the non-null elements, use nullsFirst (the nulls at the head of the list in the sorted order).
  • Use nullsLast if you want the non-null elements to appear before the nulls (the nulls pushed to the end in the sorted order).

Let us explore Comparator nullsFirst and nullsLast methods in detail.

Comparator nullsFirst

Method signature:

public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator)

Calling the Comparator.nullsFirst method returns a null-friendly comparator. It considers null to be less than any non-null value, i.e., when comparing a null and a non-null value, it considers the null to be lesser than the non-null element.

When both the arguments are null, they are considered as equal. If both are non-null, it uses the specified comparator to determine the order. If the specified comparator is null, then it considers all non-null values to be equal.
List<String> fruits = Arrays.asList("pear", "apple", "grapes", null, "orange");
fruits.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(fruits);

We use a Comparator returned by the nullsFirst method and pass a natural ordering comparator as the underlying comparator it must use. This outputs,

[null, apple, grapes, orange, pear]

It orders null before any non-null string and the rest of the non-null values are ordered by their natural ordering.

If we had passed Comparator.reverseOrder() as the comparator to the nullsFirst method, it would sort the non-null values in the reverse order.

fruits.sort(Comparator.nullsFirst(Comparator.reverseOrder()));
System.out.println(fruits); //[null, pear, orange, grapes, apple]

Finally, passing null as the underlying comparator treats all non-null values as equal. Hence it does not change the ordering of any of the non-null values.

List<String> fruits = fruits = Arrays.asList("pear", "apple", "grapes", null, "orange");
fruits.sort(Comparator.nullsFirst(null));
System.out.println(fruits); //[null, pear, apple, grapes, orange]

Comparator nullsLast

Method signature:

public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator)

Calling the Comparator.nullsLast method returns a null-friendly comparator which considers null to be greater than any non-null value i.e., when comparing a null and a non-null value, it considers the null to be greater than the non-null element (thus it orders null after the non-null element).

Like the nullsFirst method, when both the arguments are null, they are considered as equal. If both are non-null, it uses the specified comparator to determine the order. If the specified comparator is null, then it considers all non-null values to be equal.
 
Let us sort the same list by moving nulls to the end of the list using nullsLast.
List<String> fruits = Arrays.asList("pear", "apple", "grapes", null, "orange");
fruits.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(fruits); //[apple, grapes, orange, pear, null]

Sorting the non-null values in reverse order gives us,

fruits.sort(Comparator.nullsLast(Comparator.reverseOrder()));
System.out.println(fruits);//[pear, orange, grapes, apple, null]

The NullComparator class

The nullsFirst and nullsLast method returns an instance of NullComparator class. It is a package-private static class of the Comparator class. The implementation of the compare method of the NullComparator is,

private final boolean nullFirst;
// if null, non-null Ts are considered equal
private final Comparator<T> real;

@Override
public int compare(T a, T b) {
    if (a == null) {
        return (b == null) ? 0 : (nullFirst ? -1 : 1);
    } else if (b == null) {
        return nullFirst ? 1: -1;
    } else {
        return (real == null) ? 0 : real.compare(a, b);
    }
}

It has a boolean nullFirst flag and the underlying real comparator. The nullFirst flag is set to true when we call Comparator.nullsFirst and false when we call Comparator.nullsLast. 

We can say that it uses the Decorator Design Pattern since it adds additional responsibility to handle nulls (when one or more of the arguments passed to the compare method is null). We can see the implementation as:
  • Case 1: When both a and b are null – it returns 0.
  • Case 2: If a is null and b is non-null 
    • When nullFirst is true, it returns -1 (a before b).
    • Else it returns 1 (b before a).
  • Case 3: If b is null (now a is non-null)
    • When nullFirst is true, it returns 1 (b before a)
    • Else it returns -1 (a before b)
  • Case 4: When both a and b are non-null:
    • If the underlying comparator is present, it uses it to compare the ordering between the two arguments.
    • If the comparator is null, it considers a and b as equal and hence it returns 0.

Sorting a list by various comparators

Shown below is the Student class. It has name, department, birthday and GPA details for a Student. The birthday is represented using a LocalDate.

public class Student {
    private String name;
    private String department;
    private LocalDate birthDay;
    private double gpa;

    public Student(String name, String department, LocalDate birthDay, double gpa) {
        this.name = name;
        this.department = department;
        this.birthDay = birthDay;
        this.gpa = gpa;
    }

    public String getName() {
        return name;
    }

    public String getDepartment() {
        return department;
    }

    public LocalDate getBirthDay() {
        return birthDay;
    }

    public double getGpa() {
        return gpa;
    }

    @Override
    public String toString() {
        return name;
    }
}

Let us create a list of Student objects. Printing them prints the name of the student (see the toString implementation above).

List<Student> students = Arrays.asList(
        new Student("Michael", "CS", LocalDate.of(2000, 5, 3), 4.5),
        new Student("Shaun", "Math", LocalDate.of(2000, 11, 10), 4.1),
        new Student("Charlie", "CS", LocalDate.of(2000, 7, 9), 3.0),
        new Student("Smith", "Math", LocalDate.of(1999, 7, 21), 3.1)
);

System.out.println(students);//[Michael, Shaun, Charlie, Smith]

I’ll sort the student list by using the GPA and the birthday fields. We will build upon these examples to see how to handle nulls.

Sorting by GPA:
Using Comparator.comparingDouble, we can sort the students by their GPA from the lowest to the greatest (the natural ordering of a double). The method reference Student::getGpa is the key extractor function used to map each Student object to a double. Since a double is Comparable, the returned comparator compares two Students by their GPA (double value).
//Sort by GPA
students.sort(Comparator.comparingDouble(Student::getGpa));
System.out.println(students); //[Charlie, Smith, Shaun, Michael]

It can be see that Charlie has the lowest GPA (3.0) followed by Smith (3.1), Shaun (4.1) and Michael (4.5).

Sorting by the birthday:
The key extractor function used here extracts the Student’s birthday (a LocalDate is also a Comparable). Hence, it sorts the students from the youngest to the oldest.
//Sort by birthday
students.sort(Comparator.comparing(Student::getBirthDay));
System.out.println(students); //[Smith, Michael, Charlie, Shaun]

Reverse Sorting

Reverse sorting by the birthday:

There are two ways we can reverse sort.
  1. By using the call to reversed() it returns a new comparator which swaps the order in which it compares.
  2. Pass an explicit comparator for comparing the keys extracted by the key extractor.

To reverse a comparator, call the reversed method on it.

students.sort(Comparator.comparing(Student::getBirthDay).reversed());
System.out.println(students); //[Shaun, Charlie, Michael, Smith]

The second method is a bit interesting. We use the two argument method of the comparing method, which accepts a key extracting function and an explicit comparator to compare the keys. 

In the earlier post on Comparator, we used an explicit comparator when the mapped type of the key extracting function is not a Comparable. But here, though the mapped type of the key extractor is a Comparable (a LocalDate), we use a Comparator to order them the other way (reverse the ordering). Hence, it sorts the list of students from the oldest to the youngest.

students.sort(Comparator.comparing(Student::getBirthDay, Comparator.reverseOrder()));
System.out.println(students); //[Shaun, Charlie, Michael, Smith]

Using a Comparator with null in the collection

The student list now has a couple of null values.

List<Student> students = Arrays.asList(
        new Student("Michael", "CS", LocalDate.of(2000, 5, 3), 4.5),
        new Student("Shaun", "Math", LocalDate.of(2000, 11, 10), 4.1),
        null,
        new Student("Smith", "Math", LocalDate.of(1999, 7, 21), 3.1),
        null
);

If we attempt to sort them by their birthday as we did earlier, we would get a NullPointerException.

//Throws a java.lang.NullPointerException
students.sort(Comparator.comparing(Student::getBirthDay));

This happens because the comparators returned by the comparing method do not handle nulls. Specifically, since one of the element is null, it calls getBirthday() method on a null.

To solve this, we can use nullsFirst and nullsLast Comparators and pass the comparator that compares by date as the underlying comparator to use for non-null values.

students.sort(Comparator.nullsLast(Comparator.comparing(Student::getBirthDay)));
System.out.println(students); //[Smith, Michael, Shaun, null, null]

students.sort(Comparator.nullsFirst(Comparator.comparing(Student::getBirthDay)));
System.out.println(students); //[null, null, Smith, Michael, Shaun]

Using a Comparator when the field to compare with is null

Let us see what happens when one of the birthday fields is null for a Student.

List<Student> students = Arrays.asList(
        new Student("Michael", "CS", LocalDate.of(2000, 5, 3), 4.5),
        new Student("Shaun", "Math", LocalDate.of(2000, 11, 10), 4.1),
        new Student("Charlie", "CS", null, 3.0),
        new Student("Smith", "Math", LocalDate.of(1999, 7, 21), 3.1)
);

We have made the birthdate of Charlie as null. Trying to sort it using birthday results in a NullPointerException.

students.sort(Comparator.comparing(Student::getBirthDay));

Unlike the earlier case where one of the students was null, here the mapped value by the key extractor results in a null. This fails with a NullPointerException either when it calls the compareTo on a null or in the comparator function of LocalDate since it does not handle nulls (depending on whether the first or the second value is null).

Let us try to use the Comparator.nullsFirst.

students.sort(Comparator.nullsFirst(Comparator.comparing(Student::getBirthDay)));

To your surprise, this too fails with a NullPointerException. This is because the null-friendly comparator works only when one or both of the parameters it receives is null. 

Here, the argument passed is of type Student and it is not null. Hence, it delegates to the underlying comparator and we have the same problem as seen before this.
 
To handle this, let us use the two parameter overload of the comparing method. We pass a key extractor that extracts the student’s birthday and pass a null-friendly comparator to compare the Dates. If the date is null, the null-friendly comparator returned by nullsFirst or nullsLast handles it.

students.sort(Comparator.comparing(Student::getBirthDay,
        Comparator.nullsFirst(Comparator.naturalOrder())));
System.out.println(students);//[Charlie, Smith, Michael, Shaun]

students.sort(Comparator.comparing(Student::getBirthDay, 
        Comparator.nullsLast(Comparator.naturalOrder())));
System.out.println(students); //[Smith, Michael, Shaun, Charlie]

Note that we pass Comparator.naturalOrder to the nullsFirst and the nullsLast method. Charlie is the Student with a null birthdate and hence it gets to the first or the last depending on if we use nullsFirst or nullsLast.

If we wanted to sort by reverse order of date, pass Comparator.reverseOrder as the underlying comparator.

students.sort(Comparator.comparing(Student::getBirthDay,
        Comparator.nullsLast(Comparator.reverseOrder())));
System.out.println(students); //[Shaun, Michael, Smith, Charlie]

Calling reversed() at the built comparator reverses the entire order.

students.sort(Comparator.comparing(Student::getBirthDay,
        Comparator.nullsLast(Comparator.naturalOrder())).reversed());
System.out.println(students);//[Charlie, Shaun, Michael, Smith]

Conclusion

We learnt about the Comparator nullsFirst and nullsLast method in this post. First, we looked at the problem of using a Comparator when a collection has null elements in it. Second, we learnt about the Comparator nullsFirst and nullsLast methods and saw how to use them. Third, we saw how nullsFirst and nullsLast are implemented. Finally, we used them on a collection of Student objects for various scenarios.

References

Leave a Reply