编写和组合比较器
使用 Lambda 表达式实现比较器
由于函数式接口的定义,在 JDK 2 中引入的旧的 Comparator<T>
接口已成为函数式接口。因此,可以使用 Lambda 表达式实现比较器。
以下是 Comparator<T>
接口的唯一抽象方法
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
比较器的约定如下
- 如果
o1 < o2
,则compare(o1, o2)
应返回一个负数 - 如果
o1 > o2
,则compare(o1, o2)
应返回一个正数 - 在所有情况下,
compare(o1, o2)
和compare(o2, o1)
应具有相反的符号。
在 o1.equals(o2)
为 true
的情况下,o1
和 o2
的比较并不严格要求返回 0。
如何创建实现自然顺序的整数比较器?您可以使用本教程开头介绍的方法
Comparator<Integer> comparator = (i1, i2) -> Integer.compare(i1, i2);
您可能已经注意到,此 Lambda 表达式也可以用非常好的绑定方法引用以这种方式编写
Comparator<Integer> comparator = Integer::compare;
避免使用
(i1 - i2)
实现此比较器。即使此模式似乎有效,但在某些情况下它不会产生正确的结果。
此模式可以扩展到您需要比较的任何内容,只要您遵循比较器的约定。
Comparator
API 进一步发展,通过提供一个非常有用的 API 以更易读的方式创建比较器。
使用工厂方法创建比较器
假设您需要创建一个比较器来以非自然方式比较字符字符串:最短的字符串小于最长的字符串。
这样的比较器可以这样编写
Comparator<String> comparator =
(s1, s2) -> Integer.compare(s1.length(), s2.length());
您在上一部分中了解到,可以链接和组合 Lambda 表达式。此代码是这种组合的另一个示例。实际上,您可以这样重写它
Function<String, Integer> toLength = String::length;
Comparator<String> comparator =
(s1, s2) -> Integer.compare(
toLength.apply(s1),
toLength.apply(s2));
现在您可以看到,此 Comparator
的代码仅依赖于名为 toLength
的 Function
。因此,可以创建一个工厂方法,该方法将此函数作为参数并返回相应的 Comparator<String>
。
toLength
函数的返回值类型仍然存在约束:它必须是可比较的。在这里,它运行良好,因为您可以始终使用自然顺序比较整数,但您需要牢记这一点。
JDK 中确实存在这样的工厂方法:它已直接添加到 Comparator
接口中。因此,您可以这样编写前面的代码
Comparator<String> comparator = Comparator.comparing(String::length);
此 comparing()
方法是 Comparator
接口的静态方法。它将 Function
作为参数,该参数应返回 Comparable
的扩展类型。
假设您有一个具有 getName()
getter 的 User
类,并且您需要根据其名称对用户列表进行排序。您需要编写的代码如下
List<User> users = ...; // this is your list
Comparator<User> byName = Comparator.comparing(User::getName);
users.sort(byName);
链接比较器
您所在的公司目前对您提供的 Comparable<User>
感到非常满意。但是,版本 2 中有一个新的需求:User
类现在具有 firstName
和 lastName
,您需要生成一个新的 Comparator
来处理此更改。
编写每个比较器遵循与上一个相同的模式
Comparator<User> byFirstName = Comparator.comparing(User::getFirstName);
Comparator<User> byLastName = Comparator.comparing(User::getLastName);
现在您需要的是一种链接它们的方法,就像您链接 Predicate
或 Consumer
的实例一样。Comparator API 为您提供了解决方案
Comparator<User> byFirstNameThenLastName =
byFirstName.thenComparing(byLastName);
thenComparing()
方法是 Comparator
接口的默认方法,它将另一个比较器作为参数并返回一个新的比较器。当应用于两个用户时,比较器首先使用 byFirstName
比较器比较这些用户。如果结果为 0,则它将使用 byLastName
比较器比较它们。简而言之:它按预期工作。
Comparator API 进一步发展:由于 byLastName
仅依赖于 User::getLastName
函数,因此已将 thenComparing()
方法的重载添加到 API 中,该重载将此函数作为参数。因此,模式变为以下模式
Comparator<User> byFirstNameThenLastName =
Comparator.comparing(User::getFirstName)
.thenComparing(User::getLastName);
使用 Lambda 表达式、方法引用、链接和组合,创建比较器从未如此简单!
专用比较器
装箱和拆箱或基本类型也可能发生在比较器中,导致与 java.util.function
包的函数式接口情况相同的性能损失。为了解决这个问题,已添加了 comparing()
工厂方法和 thenComparing()
默认方法的专用版本。
您还可以使用以下方法创建 Comparator<T>
的实例
comparingInt(ToIntFunction<T> keyExtractor)
;comparingLong(ToLongFunction<T> keyExtractor)
;comparingDouble(ToDoubleFunction<T> keyExtractor)
.
如果您需要使用基本类型属性比较对象,并且需要避免此基本类型的装箱/拆箱,则使用这些方法。
还有一些相应的方法来链接 Comparator<T>
thenComparingInt(ToIntFunction<T> keyExtractor)
;thenComparingLong(ToLongFunction<T> keyExtractor)
;thenComparingDouble(ToDoubleFunction<T> keyExtractor)
.
想法是一样的:使用这些方法,您可以将比较链接到基于返回基本类型的专用函数构建的比较器,而不会因装箱/拆箱而产生任何性能损失。
使用其自然顺序比较可比较对象
本教程中值得一提的是几个工厂方法,它们将帮助您创建简单的比较器。
JDK 中的许多类,以及您应用程序中可能存在的许多类,都实现了 JDK 的一个特殊接口:Comparable<T>
接口。此接口有一个方法:compareTo(T other)
,它返回一个 int
。此方法用于比较 T
的此实例与 other
,遵循 Comparator<T>
接口的约定。
JDK 中的许多类已经实现了此接口。所有基本类型的包装类(Integer
、Long
等)、String
类以及 Date 和 Time API 中的日期和时间类就是这种情况。
您可以使用其自然顺序比较这些类的实例,即使用此 compareTo()
方法。Comparator API 为您提供了一个 Comparator.naturalOrder()
工厂类。它构建的比较器正是这样做的:它使用其 compareTo()
方法比较任何 Comparable
对象。
在您需要链接比较器时,拥有这样的工厂方法非常有用。以下是一个示例,您希望使用其长度比较字符字符串,然后使用其自然顺序比较(此示例使用 naturalOrder()
方法的静态导入以进一步提高可读性)
Comparator<String> byLengthThenAlphabetically =
Comparator.comparing(String::length)
.thenComparing(naturalOrder());
List<String> strings = Arrays.asList("one", "two", "three", "four", "five");
strings.sort(byLengthThenAlphabetically);
System.out.println(strings);
运行此代码将产生以下结果
[one, two, five, four, three]
反转比较器
比较器的一个主要用途当然是对对象列表进行排序。JDK 8 在 List
接口上添加了一个专门用于此的方法:List.sort()
。此方法接受一个比较器作为参数。
如果您需要以相反的顺序对之前的列表进行排序,可以使用 reversed()
方法,该方法来自 Comparator
接口。
List<String> strings =
Arrays.asList("one", "two", "three", "four", "five");
strings.sort(byLengthThenAlphabetically.reversed());
System.out.println(strings);
运行此代码将产生以下结果
[three, four, five, two, one]
处理空值
比较空对象可能会导致运行代码时出现令人讨厌的 NullPointerException
,这是您想要避免的。
假设您需要编写一个空安全的整数比较器来对整数列表进行排序。您决定遵循的约定是将所有空值推到列表的末尾,这意味着空值大于任何其他非空值。然后,您希望按自然顺序对非空值进行排序。
以下是在您可能编写的代码以实现此行为
Comparator<Integer> comparator =
(i1, i2) -> {
if (i1 == null && i1 != null) {
return 1;
} else if (i1 != null && i2 == null) {
return -1;
} else {
return Integer.compare(i1, i2);
}
};
您可以将此代码与本部分开头编写的第一个比较器进行比较,并发现可读性受到了很大影响。
幸运的是,有一种更简单的方法来编写此比较器,使用 Comparator
接口的另一个工厂方法。
Comparator<Integer> naturalOrder = Comparator.naturalOrder();
Comparator<Integer> naturalOrderNullsLast =
Comparator.nullsLast(naturalOrder());
The nullsLast()
及其兄弟方法 nullsFirst()
是 Comparator
接口的工厂方法。两者都接受一个比较器作为参数,并且只做一件事:为您处理空值,将它们推到末尾,或者将它们放在排序列表的开头。
以下是一个示例
List<String> strings =
Arrays.asList("one", null, "two", "three", null, null, "four", "five");
Comparator<String> naturalNullsLast =
Comparator.nullsLast(naturalOrder());
strings.sort(naturalNullsLast);
System.out.println(strings);
运行此代码将产生以下结果
[five, four, one, three, two, null, null, null]
更多学习
上次更新: 2023 年 2 月 24 日