使用Lambda表达式创建和组合比较器
用Lambda表达式实现比较器
多亏了函数式接口的定义,JDK 2中引入的老式Comparator接口也变成了函数式接口。因此,可以使用lambda表达式实现比较器。
下面是Comparator接口的唯一抽象方法:
@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。
对于toLength函数的返回类型仍然有一个约束:它必须是可比较的。在这里它工作得很好,因为你总是可以比较整数和它们的自然顺序,但你需要记住这一点。
JDK中确实存在这样一个工厂方法:它已经被直接添加到Comparator接口中。所以你可以这样写前面的代码:
Comparator<String> comparator = Comparator.comparing(String::length);
compare()方法是Comparator接口的一个静态方法。它接受一个Function作为参数,该参数应该返回一个Comparable扩展的类型。
假设您有一个带有getName() getter的User类,您需要根据用户的名称对用户列表进行排序。你需要写的代码如下:
List<User> users = ...; // this is your list
Comparator<User> byName = Comparator.comparing(User::getName);
users.sort(byName);
链接比较器
您所在的公司目前对您交付的Comparable非常满意。但是在版本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函数,所以API中添加了重载thenComparing()方法,该方法将此函数作为参数。所以这个模式变成了这样:
Comparator<User> byFirstNameThenLastName =
Comparator.comparing(User::getFirstName)
.thenComparing(User::getLastName);
使用lambda表达式、方法引用、链接和组合,创建比较器从来没有这么容易!
特殊的比较器
比较器中也可能出现装箱和拆箱或基本类型,这将导致与java.util.function包的函数式接口相同的性能损失。为了处理这个问题,我们添加了comparing()工厂方法和thenComparing()默认方法的特殊版本。
您还可以使用以下方法创建Comparator的实例:
- comparingInt (ToIntFunction < T > keyExtractor);
- comparingLong (ToLongFunction < T > keyExtractor);
- comparingDouble (ToDoubleFunction < T > keyExtractor)。
如果需要使用基本类型的属性比较对象,并且需要避免该基本类型的装箱/拆箱,则可以使用这些方法。
也有相应的方法连接Comparator:
- thenComparingInt (ToIntFunction < T > keyExtractor);
- thenComparingLong (ToLongFunction < T > keyExtractor);
- thenComparingDouble (ToDoubleFunction < T > keyExtractor)。
其思想是相同的:使用这些方法,您可以将比较与构建在返回基本类型的特殊函数上的比较器链接起来,而不会因为装箱/拆箱而影响性能。
使用可比较对象的自然顺序进行比较
在本教程中,有几个工厂方法值得提及,它们将帮助您创建简单的比较器。
JDK中的许多类,可能还有您的应用程序中的许多类都实现了JDK的一个特殊接口:Comparable接口。该接口有一个返回int类型的方法:compareTo(T other)。该方法用于将T的这个实例与其他实例进行比较,遵循Comparator接口的约定。
JDK的许多类已经实现了这个接口。对于基本类型(Integer、Long等)的所有包装类、String类以及date and 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()。该方法将比较器作为参数。
如果需要对前面的列表按相反的顺序排序,可以使用Comparator接口中的reversed()方法。
List<String> strings =
Arrays.asList("one", "two", "three", "four", "five");
strings.sort(byLengthThenAlphabetically.reversed());
System.out.println(strings);
输出:
[three, four, five, two, one]
处理Null值
在运行代码时,比较Null对象可能会导致讨厌的NullPointerException,这是您希望避免的。
假设您需要编写一个整数的Null 安全比较器来对整数列表进行排序。您决定遵循的约定是将所有Null 值推到列表的末尾,这意味着Null 值大于任何其他非Null 值。然后你想对非Null 值按自然顺序排序。
下面是你可以编写的代码来实现这种行为:
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());
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]