Java 数组排序时 Comparator 的使用
Java 数组排序时 Comparator 的使用
Arrays.sort
Java 对数组排序可使用Arrays.sort方法。该方法有以下版本:
public static void sort(t[] a) {}
public static void sort(t[] a, int fromIndex, int toIndex) {}
public static void sort(Object[] a) {}
public static void sort(Object[] a, int fromIndex, int toIndex) {}
public static <T> void sort(T[] a, Comparator<? super T> c) {}
public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)
t[]中的t表示基本数据类型。
对于基本类型无法提供Comparator来定制比较器。不过,基本类型可以装箱,可先将其装箱成对象数组,然后对该数组使用指定的比较器排序,最后再拆箱成基本类型数组。这可以使用流来进行处理,如将int[] arr进行反序:
int[] arr = new int[16];
// fill arr array
arr = Arrays.stream(arr).boxed().sorted(Collections.reverseOrder()).mapToInt(Integer::intValue).toArray();
使用Arrays.sort的泛型版本进行数组排序时,必须提供一个Comparator比较器,数组中的元素是根据该比较器来确定顺序的。
Comparator[1]
Comparator本身是一个函数式接口,但它提供了一些方法来让我们获取Comparator对象(你也可以自己实现Comparator接口并创建对象)。
comparing
静态方法comparing实现根据对象的“键”比较(对象的“键”被定义为什么需要我们自己提供,即“取键函数”)。comparing方法中会以数组元素为参数调用该“取键函数”得到元素的“键”,然后使用“键”的compareTo方法。如:
String[] strings = new String[16];
// fill strings array
Arrays.sort(strings, Comparator.comparing(s -> s));
Arrays.sort(strings, Comparator.comparing(String::length));
由于这里
sort方法必须提供一个比较器,当我们想使用对象本身进行比较时,“取键函数”可以被定义为s -> s(Function::identity)。第二个
sort示例中,我们使用元素的长度进行比较。
comparing源码如下(其中的一个版本):
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) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
-
comparing返回一个Comparator对象供sort的第二个参数使用。Comparator是一个函数式接口,return语句返回 lambda 表达式简化书写。 -
由于需要调用“键”的
compareTo方法,所以U具有类型限制U extends Comparable<? super U>。为什么不是限制为
U extends Comparable<U>?因为
Comparable的泛型类型为U的父类其实也可以执行。如SubClass继承SuperClass,SuperClass实现Comparable<SuperClass>,如果是U extends Comparable<U>,这里U为SubClass就会失败(SubClass不是Comparable<SubClass>)。但SubClass是Comparable<SuperClass>,限制为U extends Comparable<? super U>,U为SubClass时,?可以匹配上SuperClass。 -
泛型
T为元素的类型,U为“键”的类型。为什么
keyExtractor类型被限制为Function<? super T, ? extends U>而不是Function<T, U>?为了更宽泛地接受类型。
-
对于
T,使用通配符?而不是T那么我们可以使用以下语句:Arrays.sort(subClasses, Comparator.comparing(SuperClass::length));这里
T为SubClass,SuperClass是SubClass的父类,SubClass包含length方法,但我们不用一定要写成SubClass::length。 -
对于
U,使用通配符?可以让“取键函数”的返回值为U的子类。当我们明确要求U的类型时(出于某种目的,如下面reversed介绍到的问题),那么我们就可以不把传入的返回类型限定为U。Arrays.sort(arr, Comparator.<ElementType, SuperClass>comparing(elemant -> new SubClass()));
-
-
返回的 lambda 表达式中,
c1、c2数组中的元素,keyExtractor.apply(c1)得到“键”,然后进行compareTo。
comparing源码如下(其中的另一个版本):
public static <T, U> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
Objects.requireNonNull(keyExtractor);
Objects.requireNonNull(keyComparator);
return (Comparator<T> & Serializable)
(c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
keyExtractor.apply(c2));
}
这个版本的方法允许我们提供自己版本的Comparator作用于“键”,而不是使用“键”的compareTo方法。如:
Person[] people = new Person[16];
// fill people array
Arrays.sort(people, Comparator.comparing(Person::getName, Comparator.comparingInt(String::length)));
这里不是使用
String的compareTo,而是使用String获取length,然后使用integer的compareTo,即比较的”不是名字“,而是”名字的长度”。
除此之外,comparing还有变体形式comparingInt、comparingLong、comparingDouble来避免装箱。如:
Person[] people = new Person[16];
// fill people array
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));
thenComparing
实例方法thenComparing用于在使用它的比较器对象在比较时,出现相等时,应该再用什么比较器对象进行比较。如:
Person[] people = new Person[16];
// fill people array
Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
Arrays.sort(people, Comparator.comparing(Person::getName).thenComparing(Comparator.reverseOrder()));
thenComparing可以直接接受一个“取键函数”,源代码中会将其转变为比较器(即comparing(keyExtractor))。也可以直接提供一个比较器。上述的第一个例子,提供“取键函数”,实现先比较“姓”,一样时比较“名”。
第二个例子,将
people按照名字排序,如果名字相同,则按照当前“自然序”的倒序排序。
thenComparing源码如下(其中的一个版本):
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
-
thenComparing的使用会有三个Comparator对象,一个是调用thenComparing方法的所属对象 A,一个是thenComparing方法参数的对象 B,还有一个是thenComparing方法的返回对象 C。sort方法使用的是对象 C,调用的compare方法为上述源码中的 lambda 表达式,而该 lambda 表达式也调用了两个compare方法。需要注意的是,这两个方法一个是 A 的,一个是 B 的(不要看成都是 C 的,compare是接口的抽象方法需要具体实现)。虽然 A、B、C 都是Comparator,但它们的定义并不一定相同。
thenComparing方法的另一个版本:
default <U> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
return thenComparing(comparing(keyExtractor, keyComparator));
}
也允许我们指定的比较器比较“键”。
thenComparing也变体形式thenComparingInt、thenComparingLong、thenComparingDouble来避免装箱。
nullsFirst 和 nullsLast
如果“取键函数”返回null,那么对null对象调用方法就会出现异常,所以我们需要考虑“键”为null的情况。
静态方法nullsFirst与nullsLast可以封装Comparator对象,使其在进行null比较时,将其放在数组头或数组尾。
nullsFirst的compare执行的源代码如下。它会检查比较对象是否为null,都不为null时才会执行被封装Comparator对象的compare方法。
@Override
public int compare(T a, T b) {
if (a == null) {
return (b == null) ? 0 : (nullFirst ? -1 : 1);
} else if (b == null) {
return nullFirst ? 1: -1;
} else {
return (real == null) ? 0 : real.compare(a, b);
}
}
natureOrder 和 reversedOrder
两者均为静态方法,分别提供自然顺序及其逆序。
reversed
实例方法reversed返回当前Comparator比较的倒序版本。不过,需要注意在方法链中使用时的泛型约束。如以下例子:
String[] strings = new String[10];
// fill strings array
Arrays.sort(strings, Comparator.naturalOrder());
Arrays.sort(strings, Comparator.reverseOrder());
Arrays.sort(strings, Comparator.naturalOrder().reversed()); // compile error
Arrays.sort(strings, Comparator.<String>naturalOrder().reversed());
Arrays.sort(strings, Comparator.comparingInt(String::length).reversed());
Arrays.sort(strings, Comparator.comparingInt(s -> s.length()).reversed()); // compile error
Arrays.sort(strings, Comparator.comparingInt((String s) -> s.length()).reversed());
上述例子中有两个例子出现了编译错误。这些调用的第二个方法需要Comparator<String>。naturalOrder、reverseOrder、reversed都返回Comparator<T>,那么为什么reversed()方法出错了且只有它出错?
reversed是实例方法,方法定义中的泛型T是类Comparator<T>的泛型。
public interface Comparator<T> {
Comparator<T> reversed() {}
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {}
public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {}
}
需要naturalOrder、reverseOrder提供一个Comparator<String>,这两个方法是静态方法,其泛型是方法的泛型而不是类的。它的泛型判定可以直接根据需求(即Comparator<String>)得出。
静态方法无法使用类的泛型,因为类的泛型要在实例化时确定,而静态方法在类的实例化之前加载,它并不知道类的泛型是啥。
reversed是实例方法,它的泛型是类的泛型。Java 没有办法通过方法链来推断泛型(目前,未来可能会)。[2]naturalOrder返回了一个Comparator<T>对象,它期望通过上下文(给它具体的类型,如把它赋给某个具体变量)来推断它的类型,但这里只用调用方法,这里就“断”了。需要的Comparator<String>没办法通过reversed方法传递给naturalOrder。在这里,可以明确指出T类型来帮助 Java 类型推断,如上述例子中的Comparator.<String>naturalOrder().reversed()与Comparator.comparingInt((String s) -> s.length()).reversed()。它们都明确指出了调用reversed方法的对象的类泛型为String,reversed方法的返回值就会是Comparator<String>类型。
[美]Cay S. Horstmann.Core Java, Volume Ⅰ-Fundamentals(Eleventh Edition).林琪等.北京:机械工业出版社,2019-09:254-255. ↩︎
Stuart Marks.Comparator.reversed() does not compile using lambda[EB/OL].[2014-08-07](2023-12-15).https://stackoverflow.com/questions/25172595/comparator-reversed-does-not-compile-using-lambda. ↩︎

浙公网安备 33010602011771号