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. ↩︎
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构