Groovy 中的比较器和排序
作者: Paul King
发布时间: 2022-07-21 03:51PM
这篇博客文章的灵感来自 Stuart Marks 在精彩的Collections Refuelled 演讲 和 博客 中的比较器示例。该博客发布于 2017 年,重点介绍了 Java 8 和 9 中 Java Collections 库的改进,包括众多比较器改进。它现在已经 5 年历史了,但对于任何使用 Java Collections 库的人来说,仍然非常值得推荐。
与原始博客不同的是,我们将使用 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
方法,使其不显示空 last 名字。
在 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
中删除空 last 名字。稍后我们将介绍 @Sortable
注释。
首先,Groovy 的 spaceship 运算符 <⇒
允许我们编写第一个 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
方法,该方法对列表的副本进行排序,而不会更改原始列表。因此,要创建按 first 名字排序的列表,我们可以使用以下代码
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,用于处理集合。我们可以使用 GQueries 来检查和排序我们的集合。以下是一个示例
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 | +-------+-------+-----+
或者我们可以按 last 名字降序排序,然后按年龄排序
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 功能都可用,包括 spaceship 运算符、sort
和 toSorted
方法以及 @Sortable
AST 转换。