To demonstrate @Canonical, I begin by reproducing the aforementioned example that specified the three annotations separately. The Groovy class used then was called TheWholePerson and is 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 above can be simplified by replacing the three specific annotations with the
@Canonical
annotation. This is shown in the next code listing.CanonicalPerson.groovy
@groovy.transform.Canonical class CanonicalPerson { String lastName String firstName }
There's not much code in the
CanonicalPerson.groovy
code listing, but there is more there than meet's the eye. For convenience, I next reproduce the test driving code listing I used in my previous post with added functionality to demonstrate CanonicalPerson
in action.demonstrateCommonMethodsAnnotationsWithCanonical.groovy
#!/usr/bin/env groovy // demonstrateCommonMethodsAnnotationsWithCanonical.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} ${personTupleConstructor2.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}." // Simplify the combination! printHeader("Simplified via Canonical") def canonicalPerson1 = new CanonicalPerson('Goldfinger', 'Auric'); def canonicalPerson2 = new CanonicalPerson('Goldfinger', 'Auric'); println "${canonicalPerson1} ${canonicalPerson1 == canonicalPerson2 ? 'IS' : 'is NOT'} same as ${canonicalPerson2}." /** * 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) }
The output from running the above script is shown next. One important observation to make from this output is that the
@Canonical
does indeed provide implicit toString()
support as well as implicit equals
support. A second observation is that the output does not return name/value pairs like the example specifying the three annotation separately did for the toString()
representation.The output from using
@Canonical
uses default settings for toString()
output. To override these settings, the @ToString
annotation should be applied in conjunction with the @Canonical
annotation as shown in the next version of the Groovy model class shown previously, this time called CanonicalToStringPerson.groovy
.CanonicalToStringPerson.groovy
@groovy.transform.Canonical @groovy.transform.ToString(includeNames = true, includeFields=true) class CanonicalToStringPerson { String lastName String firstName }
In the above code listing, the same line with
@groovy.transform.ToString(includeNames = true, includeFields=true)
that led to the Groovy class TheWholePerson
's toString()
returning field name/value pairs is added to the class using the @Canonical
annotation. This allows for customization of the @ToString
representation. When I add some lines of code to the test driving Groovy script shown above with new output indicates that name/value pairs are listed for the fields in the toString()
representation.There is an obvious benefit to using
@Canonical
if the "vanilla" versions of the three transformations it represents (ToString, EqualsAndHashCode, and TupleConstructor) are sufficient. However, once it must be overridden with one or more individual annotations for customization, it might be preferable to simply specify the individual annotations.There are several other references for additional reading and/or different perspectives on
@Canonical
. The Groovy 1.8 release notes reference John Prystash's blog post Groovy 1.8: Playing with the new @Canonical Transformation. Another useful reference is mrhaki's Groovy Goodness: Canonical Annotation to Create Mutable Class.The @Immutable AST transformation has been available since Groovy 1.6 and is preferable to
@Canonical
when the state of the Groovy object should not change after instantiation. The advantage of @Canonical
exists when the class state does need to be modified after its original instantiation, but the developer wishes to have much of the boilerplate code automatically generated.Javap Proves What @Canonical Adds
Although my test code listed above shows the value of
@Canonical
, perhaps the best way to see what it adds to a normal Groovy class is to look at the javap output of a Groovy class without any annotations and to compare that to the javap
output of a Groovy class employing the @Canonical annotation
. For the "control" Groovy class that doesn't use any of these annotations, I again borrow from my previous post and that class (Person.groovy
) is reproduced here.Person.java
class Person { String lastName String firstName }
The javap output for the simple
Person
class looks like this:javap Output for Person Class Class
Compiled from "Person.groovy" public class Person extends java.lang.Object implements groovy.lang.GroovyObject { public static transient boolean __$stMC; public static long __timeStamp; public static long __timeStamp__239_neverHappen1306808397612; public Person(); public java.lang.Object this$dist$invoke$1(java.lang.String, java.lang.Object); public void this$dist$set$1(java.lang.String, java.lang.Object); public java.lang.Object this$dist$get$1(java.lang.String); protected groovy.lang.MetaClass $getStaticMetaClass(); public groovy.lang.MetaClass getMetaClass(); public void setMetaClass(groovy.lang.MetaClass); public java.lang.Object invokeMethod(java.lang.String, java.lang.Object); public java.lang.Object getProperty(java.lang.String); public void setProperty(java.lang.String, java.lang.Object); public static void __$swapInit(); static {}; public java.lang.String getLastName(); public void setLastName(java.lang.String); public java.lang.String getFirstName(); public void setFirstName(java.lang.String); public void super$1$wait(); public java.lang.String super$1$toString(); public void super$1$wait(long); public void super$1$wait(long, int); public void super$1$notify(); public void super$1$notifyAll(); public java.lang.Class super$1$getClass(); public java.lang.Object super$1$clone(); public boolean super$1$equals(java.lang.Object); public int super$1$hashCode(); public void super$1$finalize(); static java.lang.Class class$(java.lang.String); }
The
Person
class has "get" and "set" methods for its fields because Groovy provides these out-of-the-box for its property support. Although we see that it has hashCode()
and equals(Object)
implementations from its parent class, it does not have any of its own. Now, we can contrast this output against the javap
output for the class with the @Canonical
annotation.javap Output for CanonicalToStringPerson
Compiled from "CanonicalToStringPerson.groovy" public class CanonicalToStringPerson extends java.lang.Object implements groovy.lang.GroovyObject { public static transient boolean __$stMC; public static long __timeStamp; public static long __timeStamp__239_neverHappen1306808397590; public CanonicalToStringPerson(java.lang.String, java.lang.String); public CanonicalToStringPerson(java.lang.String); public CanonicalToStringPerson(); public int hashCode(); public boolean equals(java.lang.Object); public java.lang.String toString(); public java.lang.Object this$dist$invoke$1(java.lang.String, java.lang.Object); public void this$dist$set$1(java.lang.String, java.lang.Object); public java.lang.Object this$dist$get$1(java.lang.String); protected groovy.lang.MetaClass $getStaticMetaClass(); public groovy.lang.MetaClass getMetaClass(); public void setMetaClass(groovy.lang.MetaClass); public java.lang.Object invokeMethod(java.lang.String, java.lang.Object); public java.lang.Object getProperty(java.lang.String); public void setProperty(java.lang.String, java.lang.Object); public static void __$swapInit(); static {}; public java.lang.String getLastName(); public void setLastName(java.lang.String); public java.lang.String getFirstName(); public void setFirstName(java.lang.String); public void super$1$wait(); public java.lang.String super$1$toString(); public void super$1$wait(long); public void super$1$wait(long, int); public void super$1$notify(); public void super$1$notifyAll(); public java.lang.Class super$1$getClass(); public java.lang.Object super$1$clone(); public boolean super$1$equals(java.lang.Object); public int super$1$hashCode(); public void super$1$finalize(); static java.lang.Class class$(java.lang.String); }
In the above
javap
output, we can see that the expected parameterized constructor provided by @TupleConstructor
is available as are the equals(Object)
, hashCode()
, and toString()
methods. The @Canonical
annotation and its associated AST transformation did its job.Conclusion
The introduction of
@Canonical
in Groovy 1.8 continues Groovy's theme of simplifying coding and providing for concise syntax with little unnecessary verbosity. Wikipedia's primary definition of "canonical" seems to fit the use of the newly available @Canonical
: "reduced to the simplest and most significant form possible without loss of generality." The @Canonical
annotation and underlying AST do indeed make the Groovy data class nearly as simple as possible while maintaining the canonical functionality associated with such data classes.
No comments:
Post a Comment