Wednesday, April 6, 2011

Groovy 1.8: Playing with the new @Canonical Transformation

Groovy 1.8 introduces some new transformations through the use of annotations. One I came across that I wanted to investigate was @Canonical which gives you an implementation of equals(), hashCode(), and toString(), along with tuple constructors.

These transformations can also be applied individually through the @EqualsHashCode, @ToString(), and @TupleConstructors respectively.

As I have no experience with tuple constructors, I hope to take a look at that one later. On the other hand, I have written a fair share of equals() and toString() methods (some better than others) so I'm always looking for a good shortcut for both.

To start, I took a look at @EqualsAndHashCode. I wrote the simplest of classes that has one property and tried to see if comparing two instances with the same value would work. Since the class does not override equals(), this failed as one would expected:

  class Person {
int age
}

def p1 = new Person(age:30)
def p2 = new Person(age:30)

assert p1 == p2

The failure presented by running the script:

  Assertion failed: 

assert p1 == p2
| | |
| | Person@3040c5
| false
Person@1ec459b

Adding a simple equals() method does the trick for now as the assertion passes:

  class Person {
int age

boolean equals(o) {
age == o.age
}
}

We can use the @EqualsAndHashCode annotation and remove our override of equals(). Here the assertion will stll pass:

  import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Person {
int age
}

The next question I had was whether or not there was a way to exclude some properties from taking part in the comparsion. Turns out we set the annotation to ignore certain properties using excludes.

Let's add a second property, ssn, and give both our Person instances different values:

  @EqualsAndHashCode(excludes='ssn')
class Person {
int age
String ssn
}

def p1 = new Person(age:30, ssn:'1')
def p2 = new Person(age:30, ssn:'2')

assert p1 == p2

No failures here. Turns out the same can be accomplished by declaring the ssn as private:

  @EqualsAndHashCode
class Person {
int age
private String ssn
}

Now let's add @ToString into the mix:

  import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

@EqualsAndHashCode
@ToString
class Person {
int age
String ssn
}

def p1 = new Person(age:30, ssn:'1')
def p2 = new Person(age:30, ssn:'1')

assert p1 == p2
println p1

The above script prints out:

  Person(30, 1)

This is definitely more helpful than the default we get if we don't override toString():

  Person@1e092

But it could be improved perhaps if we tell the annotation to include the name of the properties in the created String:

  @ToString(includeNames=true)

We now get the more helpful:

  Person(age:30, ssn:1)

Now let's see what happens if we replace both annotations with @Canoncial:

  @Canonical
class Person {
int age
String ssn
}

def p1 = new Person(age:30, ssn:'1')
def p2 = new Person(age:30, ssn:'1')

assert p1 == p2
println p1

The assertion passes, but we now are reverted to the String representation that didn't include the field names. An attempt to add the includeNames setting failed with an exception:

  'includeNames'is not part of the annotation groovy.transform.Canonical

I found that if added the @ToString annotation back, we get the display we want.

  @Canonical
@ToString(includeNames=true)
class Person {
int age
String ssn
}

So it appears that @Canonical applies both the @EqualsAndHashCode and @ToString annotations, but applies the defaults for each. To be more selective in the behavior of the individual annotations, we need include them individually.

There look's like there are more options for each of the transformations we've used so far. I found the Javadoc for version 1.8-rc-3 in the source release to be very helpful. A link should be made availabe on the Groovy downloads page when 1.8.0 is released.

No comments:

Post a Comment