Effective Java with Groovy - Enabling Order

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's java.util.Collections class has two sort 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:

Show Comments