Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 394
George Z. Avatar asked May 08 '20 14:05

George Z.


2 Answers

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.

like image 107
Slaw Avatar answered Nov 09 '22 20:11

Slaw


Slaw's answer helped a lot and based on his answer I got exactly what I wanted. However, as he mentions

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

will throw exception if a value of an Optional is not present. By that, we are missing most of the OptionalComparator point-sense. But luckily it can be converted to:

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

where the case of a not present value will be handled.

Also, these two methods can be created:

public static <T, U extends Optional> Comparator<T> absentFirst(Function<? super T, ? extends U> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T>) (c1, c2) -> absentFirstComparator().compare(keyExtractor.apply(c1),
            keyExtractor.apply(c2));
}

public static <T, U extends Optional> Comparator<T> absentLast(Function<? super T, ? extends U> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T>) (c1, c2) -> absentLastComparator().compare(keyExtractor.apply(c1),
            keyExtractor.apply(c2));
}

and finally I get exactly what I want by:

persons.sort(absentLast(Person::getLastName));
like image 20
George Z. Avatar answered Nov 09 '22 21:11

George Z.