The problem

In the project I was working on, I had to sort a List of objects of the following class.

public class Person {
  private String lastName;
  private LocalDate birthDate;
  // ... constructor, getters and setters omitted
}

The last name is a mandatory property, birthdate is optional, meaning it can be null. The requirement was to sort a List<Person>, first by last name, then by birthdate. This was my first attempt:

List<Person> persons;
// statements to populate persons
//sort it
persons.sort(Comparator.comparing(Person::getLastName)
            .thenComparing(Person::getBirthDate));

This throws a NullPointerException if you have persons with the same last name, and some of them have a null birthdate.

The solution

the Comparator class has two helper methods to either put the null values first either put them last, aptly named nullsFirst and nullLast. Let’s go through it step by step.

persons.sort(Comparator.comparing(Person::getLastName)
        .thenComparing(Person::getBirthDate, Comparator.nullsLast(Comparator.naturalOrder())));

We now use the thenComparing method with 2 inputs:

  • a function providing the data to sort on, called the key - in this case the key value is the birthdate;

  • a comparator that is applied to the key values coming in - we use the nullsLast comparator but this is a wrapper around the comparator to do the actual comparison. We cannot put Comparator.comparing(Person::getBirthDate), because then we are back to square one.

  • The solution is to use Comparator.naturalOrder() which uses the natural order. In case of LocalDate the natural order is from old to new, or as defined in the implementation of the Comparable interface.

Sample run

Let’s put it all together and do a test run:

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Person {
    private String lastName;
    private LocalDate birthDate;

    public Person(String lastName, LocalDate birthDate) {
        this.lastName = lastName;
        this.birthDate = birthDate;
    }

    public String getLastName() {
        return lastName;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }

    @Override
    public String toString() {
        return "Person{" +
                "lastName='" + lastName + '\'' +
                ", birthDate=" + birthDate +
                '}';
    }

    public static void main(String[] args){
        List<Person> persons = new ArrayList<>();
        persons.add(new Person("Doe", null));
        persons.add(new Person("Ray", LocalDate.now()));
        persons.add(new Person("Ray", LocalDate.now().minusDays(1)));
        persons.add(new Person("Doe", LocalDate.now().plusDays(2)));
        persons.add(new Person("Doe", LocalDate.now()));
        // sort the list
        persons.sort(Comparator.comparing(Person::getLastName)
                .thenComparing(Person::getBirthDate, Comparator.nullsLast(Comparator.naturalOrder())));
        // print the list
        persons.forEach(System.out::println);

    }
}

And the sample output:

Person{lastName='Doe', birthDate=2023-05-03}
Person{lastName='Doe', birthDate=2023-05-05}
Person{lastName='Doe', birthDate=null}
Person{lastName='Ray', birthDate=2023-05-02}
Person{lastName='Ray', birthDate=2023-05-03}