系列中的上一篇
当前教程
编写和组合比较器
这是本系列的最后一篇!

系列中的上一篇: 组合 Lambda 表达式

编写和组合比较器

 

使用 Lambda 表达式实现比较器

由于函数式接口的定义,在 JDK 2 中引入的旧的 Comparator<T> 接口已成为函数式接口。因此,可以使用 Lambda 表达式实现比较器。

以下是 Comparator<T> 接口的唯一抽象方法

@FunctionalInterface
public interface Comparator<T> {

    int compare(T o1, T o2);
}

比较器的约定如下

o1.equals(o2)true 的情况下,o1o2 的比较并不严格要求返回 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 的代码仅依赖于名为 toLengthFunction。因此,可以创建一个工厂方法,该方法将此函数作为参数并返回相应的 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 类现在具有 firstNamelastName,您需要生成一个新的 Comparator 来处理此更改。

编写每个比较器遵循与上一个相同的模式

Comparator<User> byFirstName = Comparator.comparing(User::getFirstName);
Comparator<User> byLastName = Comparator.comparing(User::getLastName);

现在您需要的是一种链接它们的方法,就像您链接 PredicateConsumer 的实例一样。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> 的实例

如果您需要使用基本类型属性比较对象,并且需要避免此基本类型的装箱/拆箱,则使用这些方法。

还有一些相应的方法来链接 Comparator<T>

想法是一样的:使用这些方法,您可以将比较链接到基于返回基本类型的专用函数构建的比较器,而不会因装箱/拆箱而产生任何性能损失。

 

使用其自然顺序比较可比较对象

本教程中值得一提的是几个工厂方法,它们将帮助您创建简单的比较器。

JDK 中的许多类,以及您应用程序中可能存在的许多类,都实现了 JDK 的一个特殊接口:Comparable<T> 接口。此接口有一个方法:compareTo(T other),它返回一个 int。此方法用于比较 T 的此实例与 other,遵循 Comparator<T> 接口的约定。

JDK 中的许多类已经实现了此接口。所有基本类型的包装类(IntegerLong 等)、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 日


系列中的上一篇
当前教程
编写和组合比较器
这是本系列的最后一篇!

系列中的上一篇: 组合 Lambda 表达式