Effective Java with Groovy - Enabling Order
This post shows how does Groovy make it convenient for you to implement Comparable and Comparator interfaces.
It's a common requirement to have the items ordered in a collection. For collections of simple types, it works out of the box as follows.
List<Integer> numbers = [2, 5, 3, 1, 4]
assert numbers.sort(false) == [1, 2, 3, 4, 5] // false to avoid modifying the original collection
Java'sjava.util.Collections
class has twosort
methods. These methods mutate the original collection, and their return type is void.
Let's consider the following class.
@ToString(includePackage = false)
class Person {
String name
int age
}
If you are new to Groovy, you might want to get a taste of Groovy from this post.
Now let's create a list of Person
objects.
List<Person> people = [
new Person(name: 'Raj', age: 30),
new Person(name: 'Leonard', age: 35),
new Person(name: 'Sheldon', age: 32),
new Person(name: 'Sheldon', age: 25),
new Person(name: 'Penny', age: 35)
]
If you invoke Collections.sort(people)
you would end up with a ClassCastException
as an instance of Person
cannot be typecasted to Comparable
(the class Person
does not implement Comparable
interface).
Interestingly if you invoke people.sort(false)
, you wouldn't get a ClassCastException
. Let's run the following code to get some insights into the behaviour.
println people
List<Person> sortedList = people.sort(false)
println sortedList
sortedList.each { println it.hashCode() }
You get the following result.
[Person(Raj, 30), Person(Leonard, 35), Person(Sheldon, 32), Person(Sheldon, 25), Person(Penny, 35)]
[Person(Raj, 30), Person(Sheldon, 25), Person(Penny, 35), Person(Sheldon, 32), Person(Leonard, 35)]
680988889
1034094674
1189084611
1511574902
2059461664
I believe now you know why the list got sorted in that way. You are right; the list got sorted using the hashCode
of individual objects.
For most practical uses, you would want the items sorted based on either name or age. Let's go ahead and make the class Person
implement Comparable
. Imagine those times when you had to implement a compareTo
method from the Comparable
interface. Did you enjoy those times? Not at all.
Preferably, what you wish to express is what you think - I want the items sorted in the ascending order of name
. You are in good hands. Groovy allows you to express precisely that as follows.
@ToString(includePackage = false)
@Sortable(includes = 'name')
class Person {
String name
int age
}
Now sort()
method works as you expected. The Groovy compiler will take care of implementing Comparable
using the instructions you provided through @Sorted
AST transformation. If you don't specify the includes
attribute(and you don't specify excludes
either), both name
and age
will be used for sorting. If two people have the same name, age gets compared, and the younger person will appear first.
Suppose you want to sort the list based on age in one of the places. You would create a Comparator
implementation for such use-cases. Groovy readily makes these Comparators
available for you, one for each field in the declaring class. An example is as follows.
people.sort(false, Person.comparatorByAge())
I hope you make use of these higher-level constructs provided by Groovy.
Note that since Java 8, we have a slightly more convenient way of creating comparators, compared previous versions of Java.
Comparator<Person> comparator = Comparator.comparing(Person::getName).thenComparing(Person::getAge);
Collections.sort(people, comparator);
If you are interested in learning more about how Effective Java applies to Groovy code, you may want to take a look at my GR8Conf EU presentation.
References: