Additional convenience methods on Comparator{s}

Michael Hixson michael.hixson at gmail.com
Fri Jan 25 21:50:08 PST 2013


...and at the risk of looking like a spammer, here is an
implementation, in case any of the details were unclear.  This will be
my last email about the matter unless I'm asked to reply.

A couple of notes:
 - I unconvinced myself of the arguments in my second email.
nullsFirst() is better than nullsFirstThen(...).
 - The difficulty of implementing nullsFirst/Last correctly, and the
fact that it cannot be implemented correctly by a third party if/when
more default methods are added to Comparator (without updates to the
third-party code), together solidified my belief that the JDK itself
needs to provide them.  Related, note that mixing Guava's
nullsFirst/Last and the new default methods is a road to disaster,
e.g. Ordering.from(...).nullsFirst().thenComparing(...) is broken.

Code inline below.

-Michael

----------

/*
 * Written by Michael Hixson and released to the public domain, as
 * explained at http://creativecommons.org/publicdomain/zero/1.0/
 */

package jdk8addon;

import java.io.Serializable;
import java.util.Comparator;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;

/**
 * Provides static utility methods for comparators, supplementing those
 * already provided by {@code java.util.Comparators}.
 *
 * @see java.util.Comparator
 * @see java.util.Comparators
 * @author Michael Hixson
 */
public final class MoreComparators {
    private MoreComparators() {
        throw new AssertionError("no instances");
    }

    /* ---------- Proposed additions to j.u.Comparator ---------- */

//  /**
//   * Construct a lexicographic order comparator with a function that
//   * extracts a key and a comparator that orders those keys.  This
//   * default implementation calls {@code
//   * thenComparing(Comparators.comparing(keyExtractor, keyOrdering))}.
//   *
//   * @param <U> the type for comparison.
//   * @param keyExtractor the function used to extract the sort key.
//   * @param keyOrdering the comparator for sort keys.
//   * @throws NullPointerException if either argument is null.
//   * @see Comparators#comparing(Function, Comparator)
//   * @see #thenComparing(Comparator)
//   * @since 1.8
//   */
//  default <U> Comparator<T> thenComparing(
//          Function<? super T, ? extends U> keyExtractor,
//          Comparator<? super U> keyOrdering) {
//      return thenComparing(
//          Comparators.comparing(keyExtractor, keyOrdering));
//  }

    /* ---------- Proposed additions to j.u.Comparators ---------- */

    /**
     * Accepts a function that extracts a sort key from a type
     * {@code T}, and returns a {@code Comparator<T>} that compares by
     * that sort key using the provided key comparator.  For example, if
     * a class {@code Person} has a {@code String}-valued getter
     * {@code getLastName}, then
     * {@code comparing(Person::getLastName, CASE_INSENSITIVE_ORDER)}
     * would return a {@code Comparator<Person>} that compares
     * {@code Person} objects by their last name, ignoring case.
     *
     * @param <T> the original element type.
     * @param <U> the type for comparison.
     * @param keyExtractor the function used to extract the sort key.
     * @param keyOrdering the comparator for sort keys.
     */
    public static <T, U> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor,
            Comparator<? super U> keyOrdering) {
        Objects.requireNonNull(keyExtractor);
        Objects.requireNonNull(keyOrdering);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyOrdering.compare(
                keyExtractor.apply(c1), keyExtractor.apply(c2));
    }

    /**
     * Returns a comparator that considers {@code null} to be less than
     * all other values, and all non-null values to be equal.
     * <p>
     * Note that the returned comparator does <em>not</em> impose
     * natural ordering on non-null values.  To obtain a null-tolerant
     * natural ordering, invoke
     * {@code nullsFirst().thenComparing(naturalOrder())}.
     * <p>
     * The null-tolerance of this comparator is propagated to the
     * comparators returned by its {@code thenComparing} and
     * {@code reverseOrder} methods.  Calls to {@code thenComparing}
     * will only adjust how non-null values are compared (preserving the
     * placement of null before non-null), whereas calls to
     * {@code reverseOrder} will also reverse the placement of null
     * relative to non-null values (nulls will be last).
     * <p>
     * The following example code sorts a list of strings, placing
     * null values in the list first, and non-null values last,
     * ordering the non-null values with
     * {@link String#CASE_INSENSITIVE_ORDER}:
     * <pre>{@code
     * strings.sort(nullsFirst().thenComparing(CASE_INSENSITIVE_ORDER));
     * }</pre>
     *
     * @param <T> the element type for comparison.
     * @return a null-tolerant comparator for values of type {@code T}.
     * @see #nullsLast()
     */
    @SuppressWarnings("unchecked") // safe; the type T is irrelevant
    public static <T> Comparator<T> nullsFirst() {
        return (Comparator<T>) NullsFirstComparator.INSTANCE;
    }

    /**
     * Returns a comparator that considers {@code null} to be greater
     * than all other values, and all non-null values to be equal.
     * <p>
     * Note that the returned comparator does <em>not</em> impose
     * natural ordering on non-null values.  To obtain a null-tolerant
     * natural ordering, invoke
     * {@code nullsLast().thenComparing(naturalOrder())}.
     * <p>
     * The null-tolerance of this comparator is propagated to the
     * comparators returned by its {@code thenComparing} and
     * {@code reverseOrder} methods.  Calls to {@code thenComparing}
     * will only adjust how non-null values are compared (preserving the
     * placement of null after non-null), whereas calls to
     * {@code reverseOrder} will also reverse the placement of null
     * relative to non-null values (nulls will be first).
     * <p>
     * The following example code sorts a list of strings, placing
     * non-null values in the list first, and null values last,
     * ordering the non-null values with
     * {@link String#CASE_INSENSITIVE_ORDER}:
     * <pre>{@code
     * strings.sort(nullsLast().thenComparing(CASE_INSENSITIVE_ORDER));
     * }</pre>
     *
     * @param <T> the element type for comparison.
     * @return a null-tolerant comparator for values of type {@code T}.
     * @see #nullsFirst()
     */
    @SuppressWarnings("unchecked") // safe; the type T is irrelevant
    public static <T> Comparator<T> nullsLast() {
        return (Comparator<T>) NullsLastComparator.INSTANCE;
    }

    /**
     * The singleton returned by {@link #nullsFirst()}.
     */
    private enum NullsFirstComparator implements NullOrdering<Object> {
        INSTANCE;

        @Override
        public int compare(Object c1, Object c2) {
            if (c1 == c2) return 0;
            if (c1 == null) return -1;
            if (c2 == null) return 1;
            return 0;
        }
    }

    /**
     * The singleton returned by {@link #nullsLast()}.
     */
    private enum NullsLastComparator implements NullOrdering<Object> {
        INSTANCE;

        @Override
        public int compare(Object c1, Object c2) {
            if (c1 == c2) return 0;
            if (c1 == null) return 1;
            if (c2 == null) return -1;
            return 0;
        }
    }

    /**
     * A comparator that can compare null values without throwing a
     * {@code NullPointerException}.  Implementations of this interface
     * must permit null arguments to {@link #compare(Object, Object)}
     * and must return zero if both arguments are null or a non-zero
     * value if only one of the arguments is null.
     * <p>
     * All of the {@code thenComparing} methods are overridden to ensure
     * that the returned comparators retain null-tolerance.  (Caveat: no
     * guarantees are made regarding the treatment of null return values
     * from {@code keyExtractor} arguments.)  The {@code reverseOrder}
     * method is not overridden because the default implementation
     * already preserves null-tolerance.
     *
     * @param <T> the element type for comparison.
     * @see #nullsFirst()
     * @see #nullsLast()
     */
    private interface NullOrdering<T> extends Comparator<T> {
        @Override
        default Comparator<T> thenComparing(
                Comparator<? super T> other) {
            Objects.requireNonNull(other);
            return (NullOrdering<T> & Serializable) (c1, c2) -> {
                int res = this.compare(c1, c2);
                if (res != 0 || c1 == c2) return res;
                return other.compare(c1, c2);
            };
        }

//      @Override
//      default <U> Comparator<T> thenComparing(
//              Function<? super T, ? extends U> keyExtractor,
//              Comparator<? super U> keyOrdering) {
//          Objects.requireNonNull(keyExtractor);
//          Objects.requireNonNull(keyOrdering);
//          return (NullOrdering<T> & Serializable) (c1, c2) -> {
//              int res = this.compare(c1, c2);
//              if (res != 0 || c1 == c2) return res;
//              return keyOrdering.compare(
//                  keyExtractor.apply(c1),
//                  keyExtractor.apply(c2));
//          };
//      }

        @Override
        default <U extends Comparable<? super U>>
        Comparator<T> thenComparing(
                Function<? super T, ? extends U> keyExtractor) {
            Objects.requireNonNull(keyExtractor);
            return (NullOrdering<T> & Serializable) (c1, c2) -> {
                int res = this.compare(c1, c2);
                if (res != 0 || c1 == c2) return res;
                return keyExtractor.apply(c1)
                    .compareTo(keyExtractor.apply(c2));
            };
        }

        @Override
        default Comparator<T> thenComparing(
                ToIntFunction<? super T> keyExtractor) {
            Objects.requireNonNull(keyExtractor);
            return (NullOrdering<T> & Serializable) (c1, c2) -> {
                int res = this.compare(c1, c2);
                if (res != 0 || c1 == c2) return res;
                return Integer.compare(
                    keyExtractor.applyAsInt(c1),
                    keyExtractor.applyAsInt(c2));
            };
        }

        @Override
        default Comparator<T> thenComparing(
                ToLongFunction<? super T> keyExtractor) {
            Objects.requireNonNull(keyExtractor);
            return (NullOrdering<T> & Serializable) (c1, c2) -> {
                int res = this.compare(c1, c2);
                if (res != 0 || c1 == c2) return res;
                return Long.compare(
                    keyExtractor.applyAsLong(c1),
                    keyExtractor.applyAsLong(c2));
            };
        }

        @Override
        default Comparator<T> thenComparing(
                ToDoubleFunction<? super T> keyExtractor) {
            Objects.requireNonNull(keyExtractor);
            return (NullOrdering<T> & Serializable) (c1, c2) -> {
                int res = this.compare(c1, c2);
                if (res != 0 || c1 == c2) return res;
                return Double.compare(
                    keyExtractor.applyAsDouble(c1),
                    keyExtractor.applyAsDouble(c2));
            };
        }
    }
}


On Thu, Jan 24, 2013 at 8:34 PM, Michael Hixson
<michael.hixson at gmail.com> wrote:
> A small followup...
>
> Two downsides of my proposed null{First,Last} methods are:
>
> 1) They produce comparators that are inconsistent with equals.
> 2) People might misinterpret them to mean "nulls first/last then
> natural order", which they're not.
>
> So instead of what I initially proposed, maybe these forms are better:
>
> public static <T> Comparator<T> nullsFirstThen(Comparator<? super T>
> comparator);
> public static <T> Comparator<T> nullsLastThen(Comparator<? super T> comparator);
>
> Then the original example becomes:
>
> Collections.sort(
>     people,
>     comparing(Person::getLastName, CASE_INSENSITIVE_ORDER)
>         .thenComparing(
>             Person::getEmailAddress,
>             nullsLastThen(CASE_INSENSITIVE_ORDER)));
>
> -Michael
>
> On Thu, Jan 24, 2013 at 7:09 PM, Michael Hixson
> <michael.hixson at gmail.com> wrote:
>> I have a suggestion for a few new convenience methods on
>> Comparator{s}.  They're meant to address the use case of: sort the
>> elements by some field, but not using the natural ordering of that
>> field's type (if it even has a natural ordering).  Most commonly,
>> that'll probably be sorting by a string field ignoring case.  In fact
>> I can't think of a single instance where I wanted to sort strings
>> while *not* ignoring case.
>>
>> Admittedly, the functionality I'm about to suggest could be achieved
>> using my own helper methods; if they aren't in the JDK, they still
>> work (save for some potentially awkward English in one case).  But I
>> think they're on par with the usefulness of the other convenience
>> methods in Comparator{s}.
>>
>>
>> On j.u.Comparator:
>>
>> default <U> Comparator<T> thenComparing(
>>     Function<? super T, ? extends U> keyExtractor,
>>     Comparator<? super U> keyComparator);
>>
>> On j.u.Comparators:
>>
>> public static <T, U> Comparator<T> comparing(
>>     Function<? super T, ? extends U> keyExtractor,
>>     Comparator<? super U> keyComparator);
>> public static <T> Comparator<T> nullsFirst();
>> public static <T> Comparator<T> nullsLast();
>>
>>
>> Example usage:
>>
>> // Sort a list of people by:
>> //  1) Last name, ignoring case
>> //  2) Email address, with no email (null) last, ignoring case
>> List<Person> people = ...
>> Collections.sort(
>>     people,
>>     comparing(Person::getLastName, CASE_INSENSITIVE_ORDER)
>>         .thenComparing(
>>             Person::getEmailAddress,
>>             nullsLast().thenComparing(CASE_INSENSITIVE_ORDER)));
>>
>>
>> Without these methods in the JDK, specifically
>> Comparator.thenComparing(Function, Comparator), the "awkward English"
>> I referred to would be:
>>     .thenComparing(comparing(Person::getEmailAddress, ...))
>>
>> For what it's worth, with these helper methods, I was able to remove
>> all dependencies on Guava's Ordering and ComparisonChain--their fluent
>> Comparator utilities--from my application.
>>
>> -Michael


More information about the lambda-libs-spec-comments mailing list