Search code examples
javalambdacollectionsoption-typecomparator

Comparator for Optional<T> with key extractor, like java.util.Comparator.comparing


Consider the following example where we are sorting people based on their last name:

public class ComparatorsExample {

    public static class Person {
        private String lastName;

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

        public String getLastName() {
            return lastName;
        }

        @Override
        public String toString() {
            return "Person: " + lastName;
        }
    }

    public static void main(String[] args) {
        Person p1 = new Person("Jackson");
        Person p2 = new Person("Stackoverflowed");
        Person p3 = new Person(null);
        List<Person> persons = Arrays.asList(p3, p2, p1);
        persons.sort(Comparator.comparing(Person::getLastName));
    }
}

Now, let's assume that getLastName returns an optional:

public Optional<String> getLastName() {
    return Optional.ofNullable(lastName);
}

Obviously persons.sort(Comparator.comparing(Person::getLastName)); will not compile since Optional (the type getLastName returns) is not a comparable. However, the value it holds is.

The first google search points us in this answer. Based on this answer we can sort persons by doing:

List<Person> persons = Arrays.asList(p3, p2, p1);
OptionalComparator<String> absentLastString = absentLastComparator(); //type unsafe
persons.sort((r1, r2) -> absentLastString.compare(r1.getLastName(), r2.getLastName()));

My question is, is it possible to have this kind of sorting using a function (key extractor) just like Comparator.comparing?

I mean something like (without caring absent values first or last):

persons.sort(OptionalComparator.comparing(Person::getLastName));

If we look into Comparator.comparing, we see the following code:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable) (c1, c2) -> {
        return keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    };
}

I tried multiple ways to make it return an OptionalComparator instead of a simple Comparator, but everything I tried and made sense to me was unable to be compiled. Is it even possible to achieve something like that? I guess type-safety cannot be achieved, since even Oracle's comparing throws a type-safety warning.

I am on Java 8.


Solution

  • You can use Comparator#comparing(Function,Comparator):

    Accepts a function that extracts a sort key from a type T, and returns a Comparator<T> that compares by that sort key using the specified Comparator.

    Here's an example based on the code in your question:

    persons.sort(comparing(Person::getLastName, comparing(Optional::get)));
    

    Basically this is using nested key extractors to ultimately compare the String objects representing the last names. Note this will cause a NoSuchElementException to be thrown if either Optional is empty. You can create a more complicated Comparator to handle empty Optionals1:

    // sort empty Optionals last
    Comparator<Person> comp =
        comparing(
            Person::getLastName,
            comparing(opt -> opt.orElse(null), nullsLast(naturalOrder())));
    persons.sort(comp);
    

    If you need to do this a lot then consider creating utility methods in a manner similar to Comparator#nullsFirst(Comparator) and Comparator#nullsLast(Comparator)1:

    // empty first, then sort by natural order of the value
    public static <T extends Comparable<? super T>> Comparator<Optional<T>> emptyFirst() {
      return emptyFirst(Comparator.naturalOrder());
    }
    
    // empty first, then sort by the value as described by the given
    // Comparator, where passing 'null' means all non-empty Optionals are equal
    public static <T> Comparator<Optional<T>> emptyFirst(Comparator<? super T> comparator) {
      return Comparator.comparing(opt -> opt.orElse(null), Comparator.nullsFirst(comparator));
    }
    
    // empty last, then sort by natural order of the value
    public static <T extends Comparable<? super T>> Comparator<Optional<T>> emptyLast() {
      return emptyLast(Comparator.naturalOrder());
    }
    
    // empty last, then sort by the value as described by the given
    // Comparator, where passing 'null' means all non-empty Optionals are equal
    public static <T> Comparator<Optional<T>> emptyLast(Comparator<? super T> comparator) {
      return Comparator.comparing(opt -> opt.orElse(null), Comparator.nullsLast(comparator));
    }
    

    Which can then be used like:

    persons.sort(comparing(Person::getLastName, emptyLast()));
    

    1. Example code simplified based on suggestions provided by @Holger. Take a look at the edit history to see what the code looked like before, if curious.