Apache Groovy sorting: Closures and the spaceship
Now that you’ve mastered Groovy Closures, let’s see how they empower you to sort data more concisely and flexibly compared to Java’s interfaces. (If you haven’t installed Groovy yet, please read the intro to this series.)
Java programmers have usually encountered the Comparator interface. This defines several methods that can be used to manage comparisons between two objects for sorting purposes. In particular, the method compare(a, b) is defined and the expected result is negative if a < b, zero if a == b, and positive if a > b. Classes that want to be sortable implement Comparator and define the compare() method.
The Comparable interface is similar but with much less flexibility (and much less complexity). It defines only one method, compareTo(other), with similar semantics. For example, if a and b are instances of a class that implements Comparable, then a.compareTo(b) should be negative if a < b, zero if a == b and positive if a > b.
Coming back to Groovy, sorting is facilitated by two things:
- The
sort()methods defined take aClosureinstance as an argument and pass to it the two arguments to be compared - The spaceship operator,
<=>, deletages to thecompareTo()method
Let’s take a look at a simple example, sorting a list of 100 integers based on default sort order (ascending):
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"
Run this:
$ 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]
$
There is also a toSorted() that sorts into a new list, leaving the input list untouched. You can ignore that for now.
This is equivalent to the following:
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"
You can see that lines 12-14 replace the call to sort() with the promised Closure instance, its two parameters, a and b, and the use of the spaceship operator to handle the comparison.
Of course, you probably would prefer the first approach if you wanted the list sorted in increasing order — the default works just fine. However, if you want the list in decreasing order, the Closure instance and spaceship operator come in quite handy. You just reverse a and b on line 13 so that it looks like this:
13 b <=> a
Now run it:
$ 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]
$
And sure enough, it is in descending order.
More complicated multi-key sorting is simple with this same structure. Here I borrow some of the temperature data you saw in the previous post on advanced closures:
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 }
Look at the list of temperature measurements defined in lines one to 20. You can see that it’ s sorted in ascending order by:
- Gender (males before females)
- Temperature when gender is the same
- Pulse when gender and temperature are the same
In line 24, you sort in ascending order by:
- Gender (females before males)
- Pulse when gender is the same
- Temperature when gender and pulse are the same
This is a good place to use the Elvis operator, as you have done here, in combo with the Groovy truth. If you’re not sure how this works, think about this:
- When
a.gender > b.gender,a.gender <=> b.genderreturns +1 which istrueand therefore the result of the first Elvis operator - When
a.gender < b.gender,a.gender <=> b.genderreturns -1 which istrueand therefore the result of the first Elvis operator - When
a.gender == b.gender,a.gender <=> b.gender returns 0 which isfalseso the second condition, namelya.pulse <=> b.pulse, is evaluated in the same way - And so on to the third condition,
a.temp <=> b.tempifa.pulse == b.pulse
Run it:
$ 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]
That’s it!
Conclusion
Sorting is everywhere, and sorting in Groovy is particularly wonderful and concise. This is thanks to the syntactic support provided by the spaceship operator and sort() – and toSorted() – methods that take a Closure argument to handle the comparison.
Notice as well how the Elvis operator and Groovy truth can simplify multi-key sorts.
