How to simplify creation of null-safe Comparator on multiple fields?

bubbles :

I have this simple comparator which compares an Action object by its date and time fields:

static final Comparator<Action> COMPARATOR = comparing(Action::date, 
nullsLast(naturalOrder())).thenComparing(Action::time, 
nullsLast(naturalOrder()));

An example of the result is shown below:

{hour: 01/01/2019, time: 15:55}
{hour: 01/01/2019, time: null}
{hour: 03/01/2019, time: 11:11}
{hour: 08/01/2019, time: 11:11}
{hour: 08/01/2019, time: null}
{hour: null, time: null}
{hour: null, time: null}

The comparator needs to include three more fields. Repeating nullsLast(naturalOrder()) every time frustrates me to no end.

How could I simplify the use of the comparator without using a 3rd party library?

Stuart Marks :

It sounds like your Action class has five fields, and you want to create a composite Comparator over all the fields, and that's also null-safe. You could do this:

    Comparator<Action> COMPARATOR1 =
        comparing(Action::date, nullsLast(naturalOrder()))
        .thenComparing(Action::time, nullsLast(naturalOrder()))
        .thenComparing(Action::foo, nullsLast(naturalOrder()))
        .thenComparing(Action::bar, nullsLast(naturalOrder()))
        .thenComparing(Action::baz, nullsLast(naturalOrder()));

If repeating nullsLast(naturalOrder()) is frustrating, then one can create some utilities to help. First, we observe that naturalOrder() returns a singleton Comparator instance. It works for any Comparable type; all it does is call the compareTo() method.

The nullsLast() wrapper simply adds null checking before delegating to its wrapped Comparator. It doesn't depend at all on the actual type it's comparing. It's not a singleton, but any nulls-last-natural-order comparator is as good as any other. Thus we can create a single instance and reuse it. We wrap it in a method in order to avoid having to cast it to the right type at every use site.

static final Comparator<?> NLNO = Comparator.nullsLast(Comparator.naturalOrder());

@SuppressWarnings("unchecked")
static <T extends Comparable<T>> Comparator<T> nlno() {
    return (Comparator<T>)NLNO;
}

This lets us do the following instead:

    Comparator<Action> COMPARATOR2 =
        comparing(Action::date, nlno())
        .thenComparing(Action::time, nlno())
        .thenComparing(Action::foo, nlno())
        .thenComparing(Action::bar, nlno())
        .thenComparing(Action::baz, nlno());

Still too verbose? We can write a method that takes a variable number of accessor methods and creates a combined comparator based on them, by reducing over thenComposing(). That looks like this:

@SafeVarargs
@SuppressWarnings("varargs")
static <C extends Comparable<C>> Comparator<Action>
comparatorWith(Function<Action, ? extends C>... extractors) {
    return Arrays.stream(extractors)
                 .map(ex -> comparing(ex, nullsLast(naturalOrder())))
                 .reduce(Comparator::thenComparing)
                 .orElseThrow(() -> new IllegalArgumentException("need at least one extractor"));
}

This enables us to write the following:

    Comparator<Action> COMPARATOR3 =
        comparatorWith(Action::date, Action::time, Action::foo, Action::bar, Action::baz);

Is it worth it? The helper method is probably more complicated than writing out a single chain of nullsLast(naturalOrder()) calls. If you need a bunch of different comparator chains, though, then the ability to reuse the helper method might be worth its complexity.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=76300&siteId=1
Recommended