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