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 thenullsLast
comparator but this is a wrapper around the comparator to do the actual comparison. We cannot putComparator.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 ofLocalDate
the natural order is from old to new, or as defined in the implementation of theComparable
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}