Loading...

May 26, 2016

Groovy Goodness: Make Class Cloneable With @AutoClone

Groovy has many AST annotations that add code to our class (the Abstract Syntax Tree - AST) before it is compiled. So the compiled class file contains the code added by the AST annotation. With the @AutoClone annotation a clone method is added and the class implements the Cloneable interface. We have different strategies to choose from to support cloning for our class.

The default strategy is to invoke super.clone() in the generated clone method. The next statements will deep copy the properties (and optional fields) from our class. If one of the properties cannot be cloned an exception is thrown. In the following example code snippet we apply the @AutoClone annotation to the classes Course and Teacher:

import groovy.transform.AutoClone

@AutoClone
class Course {
    String name
    Date date
    Teacher teacher
}

@AutoClone
class Teacher {
    String name
}

def mrhaki =
    new Teacher(name: 'mrhaki')
    
def course = 
    new Course(
        name: 'Groovy 101',
        date: new Date() + 10,
        teacher: mrhaki)

// We make a deep copy.
def secondCourse = course.clone()
assert secondCourse != course
assert !secondCourse.is(course)
assert secondCourse.teacher != course.teacher

// Change property on cloned instance.
secondCourse.name = 'Groovy 101 2nd edition'

assert secondCourse.name == 'Groovy 101 2nd edition'
assert course.name == 'Groovy 101'

We can use the excludes annotation attribute to give a list of properties that must not be cloned:

import groovy.transform.AutoClone

// Do not clone the teacher property.
@AutoClone(excludes = ['teacher'])
class Course {
    String name
    Date date
    Teacher teacher
}

@AutoClone()
class Teacher {
    String name
}

def mrhaki =
    new Teacher(name: 'mrhaki')
    
def course = 
    new Course(
        name: 'Groovy 101',
        date: new Date() + 10,
        teacher: mrhaki)

// We make a deep copy.
def secondCourse = course.clone()
assert secondCourse != course
assert !secondCourse.is(course)
// Only the teacher property is
// a shallow copy.
assert secondCourse.teacher == mrhaki
assert secondCourse.teacher.is(mrhaki)

// Change property on teacher property on cloned instance.
secondCourse.teacher.name = 'hubert'

assert secondCourse.teacher.name == 'hubert'
assert course.teacher.name == 'hubert'

To include fields as well as properties we must set the annotation attribute includeFields to true.

If we want to invoke the default constructor of our class in the clone method we must use the clone style AutoCloneStyle.SIMPLE. In the generated clone method the constructor is invoked followed by copying the properties:

import groovy.transform.AutoClone
import static groovy.transform.AutoCloneStyle.SIMPLE

@AutoClone(style = SIMPLE)
class Course {
    String name
    Date date
    Teacher teacher
    
    static int counter
    
    Course() {
        counter++
    }
}

@AutoClone(style = SIMPLE)
class Teacher {
    String name
    
    static int counter
    
    Teacher() {
        counter++
    }
}

def mrhaki =
    new Teacher(name: 'mrhaki')
    
def course = 
    new Course(
        name: 'Groovy 101',
        date: new Date() + 10,
        teacher: mrhaki)
        
def otherCourse = course.clone()

// Constructor is invoked twice:
// once by ourselves to create a 
// course, the other by the clone()
// method added by @AutoClone.
assert course.counter == 2
assert course.teacher.counter == 2

The last clone style we can choose is AutoCloneStyle.COPY_CONSTRUCTOR. This time the annotation will add a protected constructor that takes another object of the same type as argument. This new constructor is used in the generated clone method. This style is useful if we have final read-only properties that can only be set via the constructor:

import groovy.transform.AutoClone
import static groovy.transform.AutoCloneStyle.COPY_CONSTRUCTOR

@AutoClone(style = COPY_CONSTRUCTOR)
class Course {
    final String name
    final Date date
    final Teacher teacher
    
    Course(
        final String name,
        final Date date,
        final Teacher teacher) {
        
        this.name = name
        this.date = date
        this.teacher = teacher
    }
}

@AutoClone(style = COPY_CONSTRUCTOR)
class Teacher {
    final String name
    
    Teacher(final String name) {
        this.name = name
    }
}

def mrhaki =
    new Teacher('mrhaki')
    
def course = 
    new Course(
        'Groovy 101',
        new Date() + 10,
        mrhaki)
        
def secondCourse = course.clone()
assert secondCourse != course
assert !secondCourse.is(course)
assert secondCourse.teacher != mrhaki
assert !secondCourse.teacher.is(mrhaki)

This annotation was already available since Groovy 1.8.

Written with Groovy 2.4.6.