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.PersonToString.groovy
@groovy.transform.ToString 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.
PersonEqualsAndHashCode.groovy
@groovy.transform.EqualsAndHashCode 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.
PersonTupleConstructor.groovy
@groovy.transform.TupleConstructor 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.TheWholePerson.groovy
@groovy.transform.TupleConstructor @groovy.transform.EqualsAndHashCode @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.
demonstrateCommonMethodsAnnotations.groovy
#!/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.
Conclusion
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:
Post a Comment