Why compareTo throws NPE using a comparator who doesn’t do that when user separatedly

Tags: , ,



I created a comparator that never throws NPE. However, when using it inside compareTo, it throws NPE. Why?

public class Person implements Comparable<Person> {
    public static final Comparator<Person> BIRTHDATE_ASCENDING_NULLS_FIRST = Comparator
            .nullsFirst(Comparator.comparing(Person::getBirthDate, Comparator.nullsFirst(Comparator.naturalOrder())));

    private String name;
    private LocalDate birthDate;

    public Person() {
        super();
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(LocalDate birthDate) {
        this.birthDate = birthDate;
    }

    @Override
    public String toString() {
        return name + " was born on " + (birthDate == null ? "???" : birthDate);
    }

    @Override
    public int compareTo(Person other) {
        // BEGIN ADITIONAL TESTS
        if (other == null) {
            return 1;
        } else if (getBirthDate() == null ^ other.getBirthDate() == null) {
            // nulls first
            return getBirthDate() == null ? -1 : 1;
        } else if (getBirthDate() == null) {
            // both are null
            return 0;
        }
        System.out.println(this.toString() + ", " + other.toString());
        // END ADITIONAL TESTS
        return BIRTHDATE_ASCENDING_NULLS_FIRST.compare(this, other);
    }

    @Override
    public int hashCode() {
        int result = 1;

        result = 31 * result + (birthDate == null ? System.identityHashCode(this) : birthDate.hashCode());

        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        return Objects.equals(birthDate, ((Person) obj).getBirthDate());
    }

    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(null);
        people.add(new Person("John", null));
        people.add(new Person("Mary", LocalDate.now().minusYears(20)));
        people.add(new Person("George", LocalDate.now().minusYears(10)));

        Collections.sort(people, BIRTHDATE_ASCENDING_NULLS_FIRST);
        System.out.println(people);

        Collections.sort(people);
        System.out.println(people);

        Collections.sort(people, BIRTHDATE_ASCENDING_NULLS_FIRST.reversed());
        System.out.println(people);

        // This one throws NPE
        Collections.sort(people);
        System.out.println(people);
    }

When expliciting the comparator on Collections.sort call, sort operation doesn’t use compareTo implementation, as expected.
When not doing this, sort operation uses the implementation of compareTo. Since this method calls the exact same comparator, why do I get NPE here? I mean, why the comparator doesn’t handle NPE when being called from compareTo?

Answer

The other calls to sort don’t throw NPE because they call Comparator.compare. The call probably looks something like this:

theComparatorYouPassedToSort.compare(item1, item2)

An actual example is located at TimSort.java line 355:

if (c.compare(a[runHi++], a[lo]) < 0) {

However, the sort overload that doesn’t take a Comparator (the one that throws a NPE), calls Person.compareTo. The call looks something like:

item1.compareTo(item2)

An actual example is located at ComparableTimSort.java line 321:

while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)

Now, no matter what null handling the inner logic of compareTo does, the above will throw a NPE if item1 is null. Your implementation of compareTo only prevents an NPE if item2, the parameter is null. It can handle a null parameter, but it can’t change how Person.compare is called. If it is called on a null object, an NPE is still thrown.



Source: stackoverflow