Groovy™ 中的比较器和排序
发布时间:2022-07-21 03:51 PM
这篇博客文章的灵感来源于 Stuart Marks 精彩的 Collections Refuelled 演讲和博客中关于 Comparator 的例子。那篇博客写于 2017 年,重点介绍了 Java 8 和 9 中 Java Collections 库的改进,包括大量的 Comparator 改进。虽然现在已经有 5 年历史了,但对于任何使用 Java Collections 库的人来说,仍然强烈推荐。
与原始博客示例中的 Student
类不同,我们将使用一个 Celebrity
类(以及后来的记录),它具有相同的 first
和 last
名字段以及一个额外的 age
字段。我们最初将按 last
姓氏进行比较,空值优先于非空值,然后按 first
姓氏,最后按 age
进行比较。
与原始博客一样,我们将处理空值,例如,只知道一个名字的名人。
Java 比较器回顾
如果用 Java 编写,我们的
Celebrity
类看起来像这样
public class Celebrity { // Java
private String firstName;
private String lastName;
private int age;
public Celebrity(String firstName, int age) {
this(firstName, null, age);
}
public Celebrity(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "Celebrity{" +
"firstName='" + firstName +
(lastName == null ? "" : "', lastName='" + lastName) +
"', age=" + age +
'}';
}
}
如果作为 Java 记录(JDK16+)会看起来好得多,但我们暂时将遵循原始博客示例的精神。这是相当标准的样板代码,实际上大部分是由 IntelliJ IDEA 生成的。唯一稍微有趣的地方是我们调整了 toString
方法,使其不显示空的姓氏。
在 JDK 8 中,使用旧式比较器编码,创建和排序一些名人的主应用程序可能看起来像这样
import java.util.ArrayList; // Java
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Celebrity> celebrities = new ArrayList<>();
celebrities.add(new Celebrity("Cher", "Wang", 63));
celebrities.add(new Celebrity("Cher", "Lloyd", 28));
celebrities.add(new Celebrity("Alex", "Lloyd", 47));
celebrities.add(new Celebrity("Alex", "Lloyd", 37));
celebrities.add(new Celebrity("Cher", 76));
Collections.sort(celebrities, (c1, c2) -> {
String f1 = c1.getLastName();
String f2 = c2.getLastName();
int r1;
if (f1 == null) {
r1 = f2 == null ? 0 : -1;
} else {
r1 = f2 == null ? 1 : f1.compareTo(f2);
}
if (r1 != 0) {
return r1;
}
int r2 = c1.getFirstName().compareTo(c2.getFirstName());
if (r2 != 0) {
return r2;
}
return Integer.compare(c1.getAge(), c2.getAge());
});
System.out.println("Celebrities:");
celebrities.forEach(System.out::println);
}
}
当我们运行这个例子时,输出看起来像这样
Celebrities: Celebrity{firstName='Cher', age=76} Celebrity{firstName='Alex', lastName='Lloyd', age=37} Celebrity{firstName='Alex', lastName='Lloyd', age=47} Celebrity{firstName='Cher', lastName='Lloyd', age=28} Celebrity{firstName='Cher', lastName='Wang', age=63}
正如原始博客中所指出的那样,这段代码相当繁琐且容易出错,通过 JDK8 中的比较器改进可以大大改善。
import java.util.Arrays; // Java
import java.util.List;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;
public class Main {
public static void main(String[] args) {
List<Celebrity> celebrities = Arrays.asList(
new Celebrity("Cher", "Wang", 63),
new Celebrity("Cher", "Lloyd", 28),
new Celebrity("Alex", "Lloyd", 47),
new Celebrity("Alex", "Lloyd", 37),
new Celebrity("Cher", 76));
celebrities.sort(comparing(Celebrity::getLastName, nullsFirst(naturalOrder())).
thenComparing(Celebrity::getFirstName).thenComparing(Celebrity::getAge));
System.out.println("Celebrities:");
celebrities.forEach(System.out::println);
}
}
原始博客还指出了 JDK9 中用于列表创建的便捷工厂方法,您可能会在这里考虑使用它们。对于我们的情况,我们将进行就地排序,因此这些方法返回的不可变列表在这里对我们没有帮助,但是 Arrays.asList
并不比 List.of
长多少,并且非常适用于此示例。
除了更短之外,comparing
和 thenComparing
方法以及内置的比较器(如 nullsFirst
和 naturalOrdering
)允许更简单的组合性。数组列表中的排序也比早期 JDK 上与 Collections.sort
方法一起使用的排序更有效。运行示例时的输出与之前相同。
Groovy 比较器介绍
大约在 Java 发展其比较器机制的同时,Groovy 也添加了一些补充功能来解决许多相同的问题。我们将看看其中一些功能,以及如何根据需要使用我们上面看到的 JDK 改进功能。
首先,让我们创建一个 Groovy Celebrity
记录
@Sortable(includes = 'last,first,age')
@ToString(ignoreNulls = true, includeNames = true)
record Celebrity(String first, String last = null, int age) {}
并创建我们的名人列表
var celebrities = [
new Celebrity("Cher", "Wang", 63),
new Celebrity("Cher", "Lloyd", 28),
new Celebrity("Alex", "Lloyd", 47),
new Celebrity("Alex", "Lloyd", 37),
new Celebrity(first: "Cher", age: 76)
]
记录定义简洁明了。它在最近的 Java 版本中也会表现出色。Groovy 解决方案的一个优点是它可以在早期 JDK 上提供模拟记录,并且还具有一些很好的声明性转换来调整记录定义。我们可以省略 @ToString
注解,这样我们会得到一个标准的记录风格的 toString
。或者我们可以在记录定义中添加一个 toString
方法,类似于 Java 示例中所做的。使用 @ToString
允许我们以声明方式从 toString
中删除空的姓氏。我们稍后会介绍 @Sortable
注解。
首先,Groovy 的飞船操作符 <⇒
允许我们编写第一个 Java 版本中“繁琐”代码的简洁版本。它看起来像这样
celebrities.sort { c1, c2 ->
c1.last <=> c2.last ?: c1.first <=> c2.first ?: c1.age <=> c2.age
}
println 'Celebrities:\n' + celebrities.join('\n')
输出看起来像这样
Celebrities: Celebrity(first:Cher, age:76) Celebrity(first:Alex, last:Lloyd, age:37) Celebrity(first:Alex, last:Lloyd, age:47) Celebrity(first:Cher, last:Lloyd, age:28) Celebrity(first:Cher, last:Wang, age:63)
如果我们需要空值排在后面,我们还需要做一点点额外的工作,但是默认值对于手头的示例来说效果很好。
我们还可以利用前面提到的“JDK8 新功能”方法
celebrities.sort(comparing(Celebrity::last, nullsFirst(naturalOrder())).
thenComparing(c -> c.first).thenComparing(c -> c.age))
但这就是我们应该回头进一步解释 @Sortable
注解的地方。该注解与抽象语法树 (AST) 转换(简称转换)相关联,它为我们提供了一个自动的 compareTo
方法,该方法考虑了记录的属性(如果它是类,也同样如此)。由于我们提供了 includes
注解属性并提供了一系列属性名称,这些名称的顺序决定了比较器中使用的属性的优先级。我们同样可以只包含该列表中的某些名称,或者提供一个 excludes
注解属性并仅提及我们不想包含的属性。
它还将 Comparable<Celebrity>
添加到我们记录实现的接口列表中。那么,这一切意味着什么呢?这意味着我们只需编写
celebrities.sort()
与 @Sortable
注解相关联的转换也为我们提供了一些额外的比较器。为了按年龄排序,我们可以使用其中一个比较器
celebrities.sort(Celebrity.comparatorByAge())
这给出了此输出
Celebrities: Celebrity(first:Cher, last:Lloyd, age:28) Celebrity(first:Alex, last:Lloyd, age:37) Celebrity(first:Alex, last:Lloyd, age:47) Celebrity(first:Cher, last:Wang, age:63) Celebrity(first:Cher, age:76)
除了 sort
方法,Groovy 还提供了一个 toSorted
方法,它对列表的副本进行排序,而保持原始列表不变。因此,要创建一个按名字排序的列表,我们可以使用以下代码
var celebritiesByFirst = celebrities.toSorted(Celebrity.comparatorByFirst())
如果以与以前示例类似的方式输出,则会得到
Celebrities: Celebrity(first:Alex, last:Lloyd, age:37) Celebrity(first:Alex, last:Lloyd, age:47) Celebrity(first:Cher, last:Lloyd, age:28) Celebrity(first:Cher, last:Wang, age:63) Celebrity(first:Cher, age:76)
如果您是函数式编程的爱好者,您可以考虑使用 List.of
来定义原始列表,然后只在后续处理中使用 toSorted
方法调用。
融入一些语言集成查询
Groovy 还具有 GQuery(又名 GINQ)功能,它提供了一种 SQL 风格的 DSL,用于处理集合。我们可以使用 GQuery 来检查和排序我们的集合。这是一个例子
println GQ {
from c in celebrities
select c.first, c.last, c.age
}
其输出如下
+-------+-------+-----+ | first | last | age | +-------+-------+-----+ | Cher | | 76 | | Alex | Lloyd | 37 | | Alex | Lloyd | 47 | | Cher | Lloyd | 28 | | Cher | Wang | 63 | +-------+-------+-----+
在这种情况下,它使用的是 @Sortable
给我们的自然排序。
或者我们可以按年龄排序
println GQ {
from c in celebrities
orderby c.age
select c.first, c.last, c.age
}
其输出如下
+-------+-------+-----+ | first | last | age | +-------+-------+-----+ | Cher | Lloyd | 28 | | Alex | Lloyd | 37 | | Alex | Lloyd | 47 | | Cher | Wang | 63 | | Cher | | 76 | +-------+-------+-----+
或者我们可以按姓氏降序,然后按年龄排序
println GQ {
from c in celebrities
orderby c.last in desc, c.age
select c.first, c.last, c.age
}
其输出如下
+-------+-------+-----+ | first | last | age | +-------+-------+-----+ | Cher | Wang | 63 | | Cher | Lloyd | 28 | | Alex | Lloyd | 37 | | Alex | Lloyd | 47 | | Cher | | 76 | +-------+-------+-----+
结论
我们已经看到了在 Groovy 中使用比较器的小例子。所有强大的 JDK 功能都可以使用,以及飞船操作符、sort
和 toSorted
方法以及 @Sortable
AST 转换。