AST Transformations
Prerequisites: Classes and objects
AST transformations are annotations that make the compiler write
code for you. (AST — abstract syntax tree — is the compiler's
internal model of your code.) Groovy ships dozens of them in
groovy.transform, and they're one of its biggest productivity wins.
@ToString, @EqualsAndHashCode, @TupleConstructor
import groovy.transform.*
@ToString(includeNames = true)
@EqualsAndHashCode
@TupleConstructor
class Book {
String title
String author
int year
}
def b1 = new Book('Dune', 'Frank Herbert', 1965)
def b2 = new Book(title: 'Dune', author: 'Frank Herbert', year: 1965)
println b1
println b1 == b2
println([b1, b2].toSet().size())
Book(title:Dune, author:Frank Herbert, year:1965)
true
1
Three annotations replaced about 40 lines of Java: a readable
toString, correct equals/hashCode, and a positional constructor.
@Canonical and @Immutable
import groovy.transform.*
@Canonical // = @ToString + @EqualsAndHashCode + @TupleConstructor
class Tag { String name; String color = 'grey' }
@Immutable(copyWith = true)
class Money {
BigDecimal amount
String currency
Money plus(Money other) { new Money(amount + other.amount, currency) }
}
println new Tag('urgent')
def price = new Money(9.99, 'EUR')
println price + new Money(5, 'EUR')
println price.copyWith(currency: 'USD')
try {
price.amount = 1
} catch (ReadOnlyPropertyException e) {
println 'Money is immutable'
}
Tag(urgent, grey)
Money(14.99, EUR)
Money(9.99, USD)
Money is immutable
@Immutable makes all fields final, copies mutable inputs defensively,
and adds equals, hashCode, toString, and constructors.
copyWith creates a changed copy.
@Builder
import groovy.transform.ToString
import groovy.transform.builder.Builder
@Builder
@ToString(includeNames = true, ignoreNulls = true)
class Request {
String url
String method
int timeout
}
def req = Request.builder()
.url('https://example.com')
.method('GET')
.timeout(30)
.build()
println req
Request(url:https://example.com, method:GET, timeout:30)
Behavior transforms
import groovy.transform.*
class Service {
int calls = 0
@Memoized
BigInteger slowFactorial(int n) {
calls++
(1..n).inject(1G) { a, b -> a * b }
}
@Lazy List<String> config = { println '(loading config)'; ['a', 'b'] }()
}
def s = new Service()
println s.slowFactorial(25)
println s.slowFactorial(25)
println "calls: $s.calls"
println 'before config'
println s.config
println s.config.size()
15511210043330985984000000
15511210043330985984000000
calls: 1
before config
(loading config)
[a, b]
2
@Memoizedcaches a method's results per argument.@Lazycomputes a field the first time it's read.
@Delegate and @Singleton
import groovy.transform.*
class AuditedList {
@Delegate List<String> items = []
List<String> log = []
boolean add(String s) { log << "added $s"; items.add(s) }
}
@Singleton
class Settings {
String theme = 'dark'
}
def list = new AuditedList()
list.add('x')
list.add('y')
println list.size()
println list.contains('y')
println list.log
println Settings.instance.theme
println Settings.instance.is(Settings.instance)
2
true
[added x, added y]
dark
true
@Delegate forwards every method of List to the items field — the
class gets size, contains, and the rest for free, while you override
only what you need. Composition without boilerplate.
Other useful transforms
| Annotation | What it generates |
|---|---|
@Sortable |
compareTo and comparators from properties |
@AutoClone |
a clone() method |
@MapConstructor / @NamedVariant |
map-based constructors / named parameters |
@Log, @Slf4j |
a log field for a logging library |
@TypeChecked, @CompileStatic |
compile-time type checking / static compilation |
@RecordType |
record-like classes (see Enums and Records) |
Level 3 shows how to write your own transformations.