Saturday, May 14, 2011

Groovy 1.8 Transformations: @ToString, @EqualsAndHashCode, and @TupleConstructor

Groovy 1.8 introduces many new features including several new useful transformations. I discussed one of these transformations, @Log, in a previous blog post. In this post, I look at three other transformations: @ToString, @EqualsAndHashCode, and @TupleConstructor.

It is often very important in Java (and in Groovy) to correctly implement toString(), equals(Object), and hashCode() methods on Java (and Groovy) classes. Correct implementation of these methods is so important, in fact, that Josh Bloch covers their implementation in detail in Chapter 3 ("Methods Common to All Objects") of Effective Java Second Edition with Item 7 focused on equals(Object), Item 8 focused on hashCode(), and Item 9 focused on toString(). I have blogged previously about the importance of these methods in posts such as Java toString() Considerations and Basic Java hashCode and equals Demonstrations.

The @ToString annotation can be applied to a Groovy class to ensure that a default toString implementation is provided. This is demonstrated in the next code listing.

class PersonToString
   String lastName
   String firstName

As the above Groovy example demonstrates, the Groovy class remains minimalistic in size. This is thanks to Groovy's automatic support for properties and default named argument constructors. By adding one simple annotation, we also now get a useful toString method.

"Data objects" are often used in comparisons and as keys. This means it is important to properly implement the equals(Object) and hashCode() methods for such classes. Groovy 1.8 adds an annotation (@EqualsAndHashCode) to specify a transformation that makes this simple. It is demonstrated in the next code listing.

class PersonEqualsHashCode
   String lastName
   String firstName

Like Project Lombock, Groovy uses a single annotation to represent construction of two methods. The reasoning for this is that equals and hashCode should be implemented consistently.

The previous two code listings have shown Groovy classes that might be instantiated with a constructor using named arguments. This is supported out of the box by Groovy by using a map of name/value pairs for the class's attributes. Groovy 1.8 adds a new annotation, @TupleConstructor, that supports a constructor accepting ordered parameters.

class PersonTupleConstructor
   String lastName
   String firstName

The three transformations covered in this post (@ToString, @EqualsAndHashCode, and @TupleConstructor) can be used in the same class as shown in the next code listing.

@groovy.transform.ToString(includeNames = true, includeFields=true)
class TheWholePerson
   String lastName
   String firstName

The next Groovy code listing is for a script that makes use of the four Groovy classes shown above.

#!/usr/bin/env groovy
// demonstrateCommonMethodsAnnotations.groovy

def person = new Person(lastName: 'Rubble', firstName: 'Barney')
def person2 = new Person(lastName: 'Rubble', firstName: 'Barney')
def personToString = new PersonToString(lastName: 'Rockford', firstName: 'Jim')
def personEqualsHashCode = new PersonEqualsHashCode(lastName: 'White', firstName: 'Barry')
def personEqualsHashCode2 = new PersonEqualsHashCode(lastName: 'White', firstName: 'Barry')

// Demonstrate value of @ToString
printHeader("@ToString Demonstrated")
println "Person with no special transformations: ${person}"
println "Person with @ToString transformation: ${personToString}"

// Demonstrate value of @EqualsAndHashCode
printHeader("@EqualsAndHashCode Demonstrated")
println "${person} ${person == person2 ? 'IS' : 'is NOT'} same as ${person2}."
println "${personEqualsHashCode} ${personEqualsHashCode == personEqualsHashCode2 ? 'IS' : 'is NOT'} same as ${personEqualsHashCode2}."

// Demonstrate value of @TupleConstructor
printHeader("@TupleConstructor Demonstrated")
def personTupleConstructor = new PersonTupleConstructor('Whyte', 'Willard')
println "Tuple Constructor #1: ${personTupleConstructor.firstName} ${personTupleConstructor.lastName}"
def personTupleConstructor2 = new PersonTupleConstructor('Prince')  // first name will be null
println "Tuple Constructor #2: ${personTupleConstructor2.firstName} ${personTupleConstructor.lastName}"

// Combine all of it!
printHeader("Bringing It All Together")
def wholePerson1 = new TheWholePerson('Blofeld', 'Ernst')
def wholePerson2 = new TheWholePerson('Blofeld', 'Ernst')
println "${wholePerson1} ${wholePerson1 == wholePerson2 ? 'IS' : 'is NOT'} same as ${wholePerson2}."

 * Print a header using provided String as header title.
 * @param headerText Text to be included in header.
def printHeader(String headerText)
   println "\n${'='.multiply(75)}"
   println "= ${headerText}"
   println "=".multiply(75)

When the above script is executed the output appears as shown in the next screen snapshot.


There are numerous tools in Javadom (such as Apache Commons and Pojomatic) for implementing common methods such as toString, equals, and hashCode. An alternative approach of using annotations to perform similar functionality has become increasingly popular via products such as Project Lombock. For Groovy developers, Groovy 1.8 now provides this latter capability as a built-in part of the language.

No comments: