a red and white rocket ship flying through the sky

Apache Groovy 排序:闭包和宇宙飞船

Java 开发者了解 Comparator 接口 —— 一个用于排序比较的工具。但是 Groovy 提供了…
首页 » 博客 » Apache Groovy 排序:闭包和宇宙飞船

既然您已经掌握了 Groovy 闭包,让我们看看它们如何使您能够比 Java 的接口更简洁和灵活地对数据进行排序。  (如果您尚未安装 Groovy,请 阅读本系列 的 介绍。)

Java 程序员通常会遇到 Comparator 接口。它定义了几个可用于管理两个对象之间比较以进行排序的方法。 特别是,定义了方法 compare(a, b),如果 a < b,则预期结果为负数;如果 a == b,则为零;如果 a > b,则为正数。想要可排序的类实现 Comparator 并定义 compare() 方法。

Comparable 接口类似,但灵活性要差得多(复杂性也低得多)。它只定义了一个方法 compareTo(other),具有类似的语义。 例如,如果 ab 是实现 Comparable 的类的实例,那么如果 a < b,则 a.compareTo(b) 应该为负数;如果 a == b,则为零;如果 a > b,则为正数。

回到 Groovy,排序由两件事促进:

  • 定义的 sort() 方法接受 Closure 实例作为参数,并将要比较的两个参数传递给它
  • 宇宙飞船操作符 <=> 委托给 compareTo() 方法

让我们看一个简单的例子,根据默认排序顺序(升序)对 100 个整数列表进行排序

1   def l = [10,87,45,1,21,95,42,5,33,80,
2   9,22,23,10, 3,52,43,87,77,28,
3   54,35,63,85,21,39,90,45,99,82,

4   87,98,24,46,95,42,89,64,36,71,
5   13,67,47,10, 3,25,54,64,51,47,
6   13,52,55,12,60,90,45,80, 5, 9,
7   82,36,91,91,58,40,88,99,22,38,

8   46,91, 8,55,38, 1,38,49,98,35,
9   97, 7,56, 3,32,89,75, 6,52,89,
10  17,37,95,86,19,69,53,23,55,37]
       
11  println "l before sort $l"
       
12  l.sort()
       
13  println "l after sort $l"

运行此代码

$ groovy Groovy16a.groovy
l before sort [10, 87, 45, 1, 21, 95, 42, 5, 33, 80, 9, 22, 23, 
10, 3, 52, 43, 87, 77, 28, 54, 35, 63, 85, 21, 39, 90, 45, 99, 82, 87, 98, 24, 46, 95, 42, 89, 64, 36, 71, 13, 67, 47, 10, 3, 25, 54, 

64, 51, 47, 13, 52, 55, 12, 60, 90, 45, 80, 5, 9, 82, 36, 91, 91, 58, 40, 88, 99, 22, 38, 46, 91, 8, 55, 38, 1, 38, 49, 98, 35, 97, 
7, 56, 3, 32, 89, 75, 6, 52, 89, 17, 37, 95, 86, 19, 69, 53, 23, 55, 37]

l after sort [1, 1, 3, 3, 3, 5, 5, 6, 7, 8, 9, 9, 10, 10, 10, 12, 
13, 13, 17, 19, 21, 21, 22, 22, 23, 23, 24, 25, 28, 32, 33, 35, 35, 36, 36, 37, 37, 38, 38, 38, 39, 40, 42, 42, 43, 45, 45, 45, 

46, 46, 47, 47, 49, 51, 52, 52, 52, 53, 54, 54, 55, 55, 55, 56, 58, 60, 63, 64, 64, 67, 69, 71, 75, 77, 80, 80, 82, 82, 85, 86, 

87, 87, 87, 88, 89, 89, 89, 90, 90, 91, 91, 91, 95, 95, 95, 97, 98, 98, 99, 99]
$

还有一个 toSorted(),它排序到一个新列表,保持输入列表不变。 您现在可以忽略它。

这等效于以下代码

1  def l = [10,87,45,1,21,95,42,5,33,80,
2   9,22,23,10, 3,52,43,87,77,28,
3   54,35,63,85,21,39,90,45,99,82,
4   87,98,24,46,95,42,89,64,36,71,
5   13,67,47,10, 3,25,54,64,51,47,
6   13,52,55,12,60,90,45,80, 5, 9,
7   82,36,91,91,58,40,88,99,22,38,
8   46,91, 8,55,38, 1,38,49,98,35,
9   97, 7,56, 3,32,89,75, 6,52,89,
10  17,37,95,86,19,69,53,23,55,37]
       
11  println "l before sort $l"
       
12  l.sort { a, b ->
13  a <=> b
14  }
       
15  println "l after sort $l"

您可以看到第 12-14 行用承诺的 Closure 实例、它的两个参数 ab 以及使用宇宙飞船操作符来处理比较替换了对 sort() 的调用。

当然,如果您想要按升序排序列表,您可能更喜欢第一种方法 —— 默认设置效果很好。 但是,如果您想要按降序排列列表,Closure 实例和宇宙飞船操作符会非常方便。 您只需在第 13 行反转 ab,使其看起来像这样

13   b <=> a

现在运行它

$ groovy Groovy16c.groovy
l before sort [10, 87, 45, 1, 21, 95, 42, 5, 33, 80, 9, 22, 23, 10, 3, 52, 43, 87, 77, 28, 54, 35, 63, 85, 21, 39, 90, 45, 99, 82, 87, 98, 24, 46, 95, 42, 89, 64, 36, 71, 13, 67, 47, 10, 3, 25, 54, 64, 51, 47, 13, 52, 55, 12, 60, 90, 45, 80, 5, 9, 82, 36, 91, 91, 58, 40, 88, 99, 22, 38, 46, 91, 8, 55, 38, 1, 38, 49, 98, 35, 97, 7, 56, 3, 32, 89, 75, 6, 52, 89, 17, 37, 95, 86, 19, 69, 53, 23, 55, 37]
l after sort [99, 99, 98, 98, 97, 95, 95, 95, 91, 91, 91, 90, 90, 89, 89, 89, 88, 87, 87, 87, 86, 85, 82, 82, 80, 80, 77, 75, 71, 69, 67, 64, 64, 63, 60, 58, 56, 55, 55, 55, 54, 54, 53, 52, 52, 52, 51, 49, 47, 47, 46, 46, 45, 45, 45, 43, 42, 42, 40, 39, 38, 38, 38, 37, 37, 36, 36, 35, 35, 33, 32, 28, 25, 24, 23, 23, 22, 22, 21, 21, 19, 17, 13, 13, 12, 10, 10, 10, 9, 9, 8, 7, 6, 5, 5, 3, 3, 3, 1, 1]
$

果然,它是降序排列的。

对于更复杂的多键排序,相同的结构也很简单。 这里我借用了一些您在之前关于 高级闭包 的帖子中看到的温度数据

1	def l = [
2	[temp:  96.3, gender: 'male',   pulse: 70],
3	[temp:  96.7, gender: 'male',   pulse: 71],
4	[temp:  96.9, gender: 'male',   pulse: 74],
5	[temp:  97.0, gender: 'male',   pulse: 80],
6	[temp:  97.1, gender: 'male',   pulse: 73],
7	[temp:  97.1, gender: 'male',   pulse: 75],
8	[temp:  97.1, gender: 'male',   pulse: 82],
9	[temp:  97.2, gender: 'male',   pulse: 64],
10	[temp:  97.3, gender: 'male',   pulse: 69],
11	[temp:  99.0, gender: 'female', pulse: 81],
12	[temp:  99.1, gender: 'female', pulse: 80],
13	[temp:  99.1, gender: 'female', pulse: 74],
14	[temp:  99.2, gender: 'female', pulse: 77],
15	[temp:  99.2, gender: 'female', pulse: 66],
16	[temp:  99.3, gender: 'female', pulse: 68],
17	[temp:  99.4, gender: 'female', pulse: 77],
18	[temp:  99.9, gender: 'female', pulse: 79],
19	[temp: 100.0, gender: 'female', pulse: 78],
20	[temp: 100.8, gender: 'female', pulse: 77]]
       
21  println "l before sort:"
22	l.each { println it }
       
23  l.sort { a, b ->
24	a.gender <=> b.gender ?: a.pulse <=> b.pulse ?: a.temp <=> b.temp
25	}
       
26	println "l after sort:"
27	l.each { println it }

查看第 1 行到第 20 行中定义的温度测量列表。您可以看到它是按升序排序的:

  1. 性别(男性在女性之前)
  2. 性别相同时的温度
  3. 性别和温度相同时的脉搏

在第 24 行中,您按升序排序:

  1. 性别(女性在男性之前)
  2. 性别相同时的脉搏
  3. 性别和脉搏相同时的温度

这是一个使用 Elvis 操作符的好地方,就像您在这里所做的那样,与 Groovy 真值结合使用。 如果您不确定这是如何工作的,请考虑一下:

  • a.gender > b.gender 时,a.gender <=> b.gender 返回 +1,它是 true,因此是第一个 Elvis 操作符的结果
  • a.gender < b.gender 时,a.gender <=> b.gender 返回 -1,它是 true,因此是第一个 Elvis 操作符的结果
  • a.gender == b.gender 时,a.gender <=> b.gender 返回 0,它是 false,因此以相同的方式评估第二个条件,即 a.pulse <=> b.pulse
  • 依此类推,直到第三个条件 a.temp <=> b.temp,如果 a.pulse == b.pulse

运行它

$ groovy Groovy16d.groovy
l before sort:
[temp:96.3, gender:male, pulse:70]
[temp:96.7, gender:male, pulse:71]
[temp:96.9, gender:male, pulse:74]
[temp:97.0, gender:male, pulse:80]
[temp:97.1, gender:male, pulse:73]
[temp:97.1, gender:male, pulse:75]
[temp:97.1, gender:male, pulse:82]
[temp:97.2, gender:male, pulse:64]
[temp:97.3, gender:male, pulse:69]
[temp:99.0, gender:female, pulse:81]
[temp:99.1, gender:female, pulse:80]
[temp:99.1, gender:female, pulse:74]
[temp:99.2, gender:female, pulse:77]
[temp:99.2, gender:female, pulse:66]
[temp:99.3, gender:female, pulse:68]
[temp:99.4, gender:female, pulse:77]
[temp:99.9, gender:female, pulse:79]
[temp:100.0, gender:female, pulse:78]
[temp:100.8, gender:female, pulse:77]
l after sort:
[temp:99.2, gender:female, pulse:66]
[temp:99.3, gender:female, pulse:68]
[temp:99.1, gender:female, pulse:74]
[temp:99.2, gender:female, pulse:77]
[temp:99.4, gender:female, pulse:77]
[temp:100.8, gender:female, pulse:77]
[temp:100.0, gender:female, pulse:78]
[temp:99.9, gender:female, pulse:79]
[temp:99.1, gender:female, pulse:80]
[temp:99.0, gender:female, pulse:81]
[temp:97.2, gender:male, pulse:64]
[temp:97.3, gender:male, pulse:69]
[temp:96.3, gender:male, pulse:70]
[temp:96.7, gender:male, pulse:71]
[temp:97.1, gender:male, pulse:73]
[temp:96.9, gender:male, pulse:74]
[temp:97.1, gender:male, pulse:75]
[temp:97.0, gender:male, pulse:80]
[temp:97.1, gender:male, pulse:82]

就是这样!

结论

排序无处不在,而 Groovy 中的排序尤其精彩而简洁。 这要归功于宇宙飞船操作符和 sort() —— 以及 toSorted() —— 方法提供的语法支持,这些方法接受 Closure 参数来处理比较。

还要注意 Elvis 操作符Groovy 真值 如何简化多键排序。

作者

如果您喜欢这篇文章,您可能也喜欢这些