82. Useful Annotations

Groovy comes with a number of handy notations that let you easily customise a class without doing the hard work yourself. This chapter will take a brief look at some of the handier annotations in the groovy.tranform package.

You can even write your own annotations but that’s something for another book…

ToString

The toString() method is used to provide a “human readable” representation of the object. It’s available on all objects and you can override it but you can also just use the ToString annotation to make this even easier:

The ToString annotation
@groovy.transform.ToString
class Person {
    def name
    def email
    def mobile

    Person(name) {
        this.name = name
    }
}

def astrid = new Person('Astrid Smithson')

println astrid

ToString takes a few options:

  • @ToString(includeNames=true): will add in the property name, prefixing the property value
  • @ToString(includeFields=true): by default, properties are used but this adds in fields

You can use several options at once:

The ToString annotation with options
@groovy.transform.ToString(includeNames=true, includeFields=true)
class Person {
    def name
    private email
    private mobile

    Person(name) {
        this.name = name
    }
}

def astrid = new Person('Astrid Smithson')

println astrid

EqualsAndHashCode

Determining if two instances are equal is something you have to add in yourself. Groovy will agree that two variables are the same if they point to the same instance of a class:

def agentSmith1 = new Person(id: 411, name: 'Agent Smith')
def agentSmith2 = agentSmith1
assert agentSmith1 == agentSmith2

However, this isn’t Groovy being clever, it’s just seeing that agentSmith1 and agentSmith2 point to the same thing. In the code below you’ll see that a Person instance with the same id and name as another Person instance doesn’t automatically equate to them being equal:

Same, same but not equal
class Person {
    def id
    def name
}

def agentSmith1 = new Person(id: 411, name: 'Agent Smith')
def agentSmith2 = agentSmith1
def agentSmith3 = new Person(id: 411, name: 'Agent Smith')

assert agentSmith1 == agentSmith2

//This will fail
assert agentSmith2 == agentSmith3

By using the EqualsAndHashCode annotation I can tell Groovy that two instances of Person with the same id are actually equal:

The EqualsAndHashCode annotation
@groovy.transform.EqualsAndHashCode(includes='id')
class Person {
    def id
    def name
}

def agentSmith = new Person(id: 414, name: 'Agent Smith')
def agentSmith2 = new Person(id: 414, name: 'Agent X')

assert agentSmith == agentSmith2

Equality is not always easy to determine and will depend on the context in which you’re developing - my example above is likely to cause some debate as to if matching only on id is enough. The EqualsAndHashCode annotation has a few options that are worth digging into.

Canonical

The Canonical annotation brings together functionality from a suite of other annotations:

  • ToString
  • EqualsAndHashCode
  • TupleConstructor

We’ve looked at these previously so let’s jump to an example:

The Canonical annotation
@groovy.transform.Canonical
class Person {
    def name
    def email
    def mobile
}

Person phil = new Person('Phil', '041414141')

println phil

This saves you from needing to stack your annotations:

Canonical combines three annotations
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovy.transform.TupleConstructor

@ToString
@EqualsAndHashCode
@TupleConstructor
class Person {
    def name
    def email
    def mobile
}

Person phil = new Person('Phil', '041414141')

println phil

Immutable

The Immutable annotation provides similar features as Canonical but locks down new instances so that they can’t be changed after creation. Immutable objects can be very useful in systems using parallel processing or caching.

You can’t touch this - it will cause an exception
@groovy.transform.Immutable
class Person {
    String name
    String email
    String mobile
}

def krusty = new Person(name: 'Krusty')
krusty.email = 'heyhey@example.com'

The code above will fail on krusty.email = 'heyhey@example.com' as the Immutable annotation marks the email field as final.