Upgrade to Pro — share decks privately, control downloads, hide ads and more …

What's new in Apache Groovy 2.4, 2.5, 3.0....

What's new in Apache Groovy 2.4, 2.5, 3.0....

At BreizhCamp 2017, Guillaume Laforge and Cédric Champeau presented about the latest developments about Apache Groovy: in the current 2.4 branch, in the new 2.5 alpha, and what's coming in 2.6 and beyond.

Guillaume Laforge

April 21, 2017
Tweet

More Decks by Guillaume Laforge

Other Decks in Technology

Transcript

  1. Sinon quoi de neuf côté Apache Groovy? Guillaume Laforge
 @glaforge

    
 PMC Chair of 
 Apache Groovy 
 
 Developer Advocate 
 Google Cloud Platform Cédric Champeau
 @cedricchampeau 
 PMC Member of 
 Apache Groovy 
 
 Principal Engineer Gradle Inc.
  2. 2 @Canonical becomes a meta-annotation import groovy.transform.*
 
 @Canonical(includeNames =

    true)
 class Person {
 String name
 int age
 }
 
 assert new Person('Guillaume', 39).toString() ==
 'Person(name:Guillaume, age:39)'
  3. 2 @Canonical becomes a meta-annotation import groovy.transform.*
 
 @Canonical(includeNames =

    true)
 class Person {
 String name
 int age
 }
 
 assert new Person('Guillaume', 39).toString() ==
 'Person(name:Guillaume, age:39)' includeNames from @ToString
  4. 3 More control on annotation collector DUPLICATE Annotations from the

    annotation collection will always be inserted. PREFER_COLLECTOR Annotations from the collector will be added and any existing annotations with the same name will be removed. PREFER_EXPLICIT Annotations from the collector will be ignored if any existing annotations with the same name are found. PREFER_EXPLICIT_MERGED Annotations from the collector will be ignored if any existing annotations with the same name are found but any new parameters on the collector annotation will be added to existing annotations. PREFER_COLLECTOR_MERGED Annotations from the collector will be added and any existing annotations with the same name will be removed but any new parameters found within existing annotations will be merged into the added annotation.
  5. 4 Pre/post events for @TupleConstructor import groovy.transform.TupleConstructor @TupleConstructor(pre = {

    first = first?.toLowerCase() }) class Person { String first } def p = new Person('Jack') assert p.first == 'jack'
  6. 4 Pre/post events for @TupleConstructor import groovy.transform.TupleConstructor @TupleConstructor(pre = {

    first = first?.toLowerCase() }) class Person { String first } def p = new Person('Jack') assert p.first == 'jack' import groovy.transform.TupleConstructor import static groovy.test.GroovyAssert.shouldFail @TupleConstructor(pre = { assert first }) class Person { String first } def p = new Person('Jack') shouldFail { def unknown = new Person() }
  7. 5 Prevent @TupleConstructor default ctors @TupleConstructor
 class Person {
 String

    first, last
 int age
 } Generates: Person(String first, String last, int age) { /*...*/ } Person(String first, String last) { this(first, last, 0) } Person(String first) { this(first, null) } Person() { this(null) }
  8. 6 Prevent @TupleConstructor default ctors @TupleConstructor(defaults = true)
 class Person

    { String first, last
 int age
 } Generates only: Person(String first, String last, int age) { /*...*/ }
  9. 7 New @MapConstructor transformation import groovy.transform.*
 
 @TupleConstructor
 class Person

    {
 String first, last
 }
 
 @CompileStatic // optional
 @ToString(includeSuperProperties = true)
 @MapConstructor(pre = { super(args?.first, args?.last);
 args = args ?: [:] },
 post = { first = first?.toUpperCase() })
 class Author extends Person {
 String bookName
 } assert new Author(first: 'Dierk', last: 'Koenig', bookName: 'ReGinA').toString() == 'Author(ReGinA, DIERK, Koenig)'
 
 assert new Author().toString() == 'Author(null, null, null)'
  10. 7 New @MapConstructor transformation import groovy.transform.*
 
 @TupleConstructor
 class Person

    {
 String first, last
 }
 
 @CompileStatic // optional
 @ToString(includeSuperProperties = true)
 @MapConstructor(pre = { super(args?.first, args?.last);
 args = args ?: [:] },
 post = { first = first?.toUpperCase() })
 class Author extends Person {
 String bookName
 } assert new Author(first: 'Dierk', last: 'Koenig', bookName: 'ReGinA').toString() == 'Author(ReGinA, DIERK, Koenig)'
 
 assert new Author().toString() == 'Author(null, null, null)' Can decorate map ctor with pre / post- instructions
  11. 8 Properties validated in AST xforms import groovy.transform.AutoClone
 
 @AutoClone(excludes

    = 'sirName')
 class Person {
 String firstName
 String surName
 }
 
 new Person(firstName: "John", surName: "Doe").clone()
  12. 8 Properties validated in AST xforms import groovy.transform.AutoClone
 
 @AutoClone(excludes

    = 'sirName')
 class Person {
 String firstName
 String surName
 }
 
 new Person(firstName: "John", surName: "Doe").clone() Error during @AutoClone processing: 'excludes' property 'sirName' does not exist.
  13. 9 @Immutable support in class hierarchy import groovy.transform.*
 
 @EqualsAndHashCode


    class Person {
 String name
 } @Immutable
 @TupleConstructor(includeSuperProperties = true)
 @EqualsAndHashCode(callSuper = true)
 @ToString(includeNames = true, includeSuperProperties = true)
 class Athlete extends Person {
 String sport
 }
  14. 9 @Immutable support in class hierarchy import groovy.transform.*
 
 @EqualsAndHashCode


    class Person {
 String name
 } @Immutable
 @TupleConstructor(includeSuperProperties = true)
 @EqualsAndHashCode(callSuper = true)
 @ToString(includeNames = true, includeSuperProperties = true)
 class Athlete extends Person {
 String sport
 } def d1 = new Athlete('Michael Jordan', 'BasketBall')
 def d2 = new Athlete(name: 'Roger Federer', sport: 'Tennis') assert d1 != d2
 assert d1.toString() == 
 'Athlete(sport:BasketBall, name:Michael Jordan)'
 assert d2.toString() == 
 'Athlete(sport:Tennis, name:Roger Federer)'
  15. 10 @Immutable supports Optional import groovy.transform.Immutable
 
 @Immutable
 class Person

    {
 String name
 Optional<String> address
 }
 
 def p = new Person('Joe', Optional.of('Home'))
 
 assert p.toString() == 'Person(Joe, Optional[Home])'
 assert p.address.get() == 'Home'
  16. 10 @Immutable supports Optional import groovy.transform.Immutable
 
 @Immutable
 class Person

    {
 String name
 Optional<String> address
 }
 
 def p = new Person('Joe', Optional.of('Home'))
 
 assert p.toString() == 'Person(Joe, Optional[Home])'
 assert p.address.get() == 'Home' @Immutable
 class Person {
 String name
 Optional<Date> birth
 } Fails compilation, as Date is mutable
  17. 11 New @AutoImplement transformation @AutoImplement
 class MyNames extends AbstractList<String> implements

    Closeable {} class MyNames extends AbstractList<String> implements Closeable {
 String get(int param0) {
 return null
 }
 boolean addAll(Collection<? extends String> param0) {
 return false
 }
 void close() throws Exception {
 }
 int size() {
 return 0
 }
 }
  18. 12 New @AutoImplement transformation @AutoImplement(exception = IOException)
 class MyWriter extends

    Writer { } @AutoImplement(exception = UnsupportedOperationException,
 message = 'Not supported by MyIterator')
 class MyIterator implements Iterator<String> { }
  19. 12 New @AutoImplement transformation @AutoImplement(exception = IOException)
 class MyWriter extends

    Writer { } @AutoImplement(exception = UnsupportedOperationException,
 message = 'Not supported by MyIterator')
 class MyIterator implements Iterator<String> { } @AutoImplement(code = { throw new UnsupportedOperationException(
 'Should never be called but was called on ' + new Date()) })
 class EmptyIterator implements Iterator<String> {
 boolean hasNext() { false }
 }
  20. 13 :grab command in groovysh groovy:000> :grab 'com.google.guava:guava:19.0' groovy:000> import

    com.google.common.collect.BiMap ===> com.google.common.collect.BiMap
  21. 14 @Delegate on getters too class Person {
 String name

    @Delegate
 String getName() { name.reverse() } } def p = new Person(name: 'Erine') assert p.toUpperCase() == 'ENIRE'
  22. 15 JAXB marshalling shortcuts import groovy.transform.EqualsAndHashCode import javax.xml.bind.JAXBContext import javax.xml.bind.annotation.*

    @EqualsAndHashCode @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement class Person { String name int age }
  23. 15 JAXB marshalling shortcuts import groovy.transform.EqualsAndHashCode import javax.xml.bind.JAXBContext import javax.xml.bind.annotation.*

    @EqualsAndHashCode @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement class Person { String name int age } def jaxbContext = JAXBContext.newInstance(Person) def p = new Person(name: 'Marion', age: 9) def xml = jaxbContext.marshal(p) assert jaxbContext.unmarshal(xml, Person) == p
  24. 15 JAXB marshalling shortcuts import groovy.transform.EqualsAndHashCode import javax.xml.bind.JAXBContext import javax.xml.bind.annotation.*

    @EqualsAndHashCode @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement class Person { String name int age } def jaxbContext = JAXBContext.newInstance(Person) def p = new Person(name: 'Marion', age: 9) def xml = jaxbContext.marshal(p) assert jaxbContext.unmarshal(xml, Person) == p No need for createMarshaller()
  25. 16 CliBuilder supports annotations // specify parameters
 def cli =

    new CliBuilder(usage: 'groovy Greeter [option]')
 cli.a(longOpt: 'audience', args: 1, 'greeting audience')
 cli.h(longOpt: 'help', 'display usage')
 
 // parse and process parameters
 def options = cli.parse(args)
 if (options.h) cli.usage()
 else println "Hello ${options.a ? options.a : 'World'}"
  26. 17 CliBuilder supports annotations import groovy.cli.*
 
 interface GreeterI {


    @Option(shortName='h', description='display usage') Boolean help()
 @Option(shortName='a', description='greeting audience') String audience()
 @Unparsed List remaining()
 }
  27. 17 CliBuilder supports annotations import groovy.cli.*
 
 interface GreeterI {


    @Option(shortName='h', description='display usage') Boolean help()
 @Option(shortName='a', description='greeting audience') String audience()
 @Unparsed List remaining()
 } def cli = new CliBuilder(usage: 'groovy Greeter [option]')
 def argz = '--audience Groovologist'.split()
 def options = cli.parseFromSpec(GreeterI, argz)
 assert options.audience() == 'Groovologist'
  28. 18 With… tap… assert new Person().with { name = 'Guillaume'

    age = 39 return it }.toString() == 'Person(Guillaume, 39)' @Canonical
 class Person {
 String name
 int age
 }
  29. 18 With… tap… assert new Person().with { name = 'Guillaume'

    age = 39 return it }.toString() == 'Person(Guillaume, 39)' assert new Person().tap { name = 'Guillaume' age = 39 }.toString() == 'Person(Guillaume, 39)' @Canonical
 class Person {
 String name
 int age
 }
  30. 19 Miscellaneous • Map#retainAll {} and Map#removeAll {} methods •

    GDK’s createSimilarCollection() and createSimilarMap() methods support all the JDK’s collections and maps • New File#relativePath(file) method
  31. 21 A customizable JSON serializer class Person {
 String name


    String title
 int age
 String password
 Date dob
 URL favoriteUrl
 } Person person = new Person(name: 'John', title: null, 
 age: 21, password: 'secret',
 dob: Date.parse('yyyy-MM-dd', '1984-12-15'),
 favoriteUrl: new URL('http://groovy-lang.org/'))
 
 def generator = new JsonGenerator.Options()
 .excludeNulls()
 .dateFormat('yyyy@MM')
 .excludeFieldsByName('age', 'password')
 .excludeFieldsByType(URL)
 .build()
 
 assert generator.toJson(person) == 
 '{"dob":"1984@12","name":"John"}'
  32. 22 A customizable JSON serializer def generator = new JsonGenerator.Options()


    .addConverter(URL) { URL u, String key ->
 if (key == 'favoriteUrl') {
 u.getHost()
 } else {
 u
 }
 }
 .build()
  33. 22 A customizable JSON serializer def generator = new JsonGenerator.Options()


    .addConverter(URL) { URL u, String key ->
 if (key == 'favoriteUrl') {
 u.getHost()
 } else {
 u
 }
 }
 .build() Data type to customize
  34. 22 A customizable JSON serializer def generator = new JsonGenerator.Options()


    .addConverter(URL) { URL u, String key ->
 if (key == 'favoriteUrl') {
 u.getHost()
 } else {
 u
 }
 }
 .build() Data type to customize Optional key
  35. 25 AST macros: the old way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 void visit(ASTNode[] nodes, SourceUnit source) {
 ClassNode classNode = (ClassNode) nodes[1]
 
 ReturnStatement code =
 new ReturnStatement(
 new ConstantExpression("42"))
 
 MethodNode methodNode =
 new MethodNode("getMessage",
 ACC_PUBLIC, ClassHelper.make(String),
 [] as Parameter[], [] as ClassNode[],
 code)
 
 classNode.addMethod(methodNode)
 }
 }
  36. 25 AST macros: the old way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 void visit(ASTNode[] nodes, SourceUnit source) {
 ClassNode classNode = (ClassNode) nodes[1]
 
 ReturnStatement code =
 new ReturnStatement(
 new ConstantExpression("42"))
 
 MethodNode methodNode =
 new MethodNode("getMessage",
 ACC_PUBLIC, ClassHelper.make(String),
 [] as Parameter[], [] as ClassNode[],
 code)
 
 classNode.addMethod(methodNode)
 }
 } return 42
  37. 26 AST macros: the new way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 void visit(ASTNode[] nodes, SourceUnit source) {
 ClassNode classNode = (ClassNode) nodes[1]
 
 ReturnStatement simplestCode = macro { return "42" } 
 
 
 MethodNode methodNode =
 new MethodNode("getMessage",
 ACC_PUBLIC, ClassHelper.make(String),
 [] as Parameter[], [] as ClassNode[],
 code)
 
 classNode.addMethod(methodNode)
 }
 }
  38. 26 AST macros: the new way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 void visit(ASTNode[] nodes, SourceUnit source) {
 ClassNode classNode = (ClassNode) nodes[1]
 
 ReturnStatement simplestCode = macro { return "42" } 
 
 
 MethodNode methodNode =
 new MethodNode("getMessage",
 ACC_PUBLIC, ClassHelper.make(String),
 [] as Parameter[], [] as ClassNode[],
 code)
 
 classNode.addMethod(methodNode)
 }
 } macro method
  39. 26 AST macros: the new way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 void visit(ASTNode[] nodes, SourceUnit source) {
 ClassNode classNode = (ClassNode) nodes[1]
 
 ReturnStatement simplestCode = macro { return "42" } 
 
 
 MethodNode methodNode =
 new MethodNode("getMessage",
 ACC_PUBLIC, ClassHelper.make(String),
 [] as Parameter[], [] as ClassNode[],
 code)
 
 classNode.addMethod(methodNode)
 }
 } macro method Different variants: macro(Closure), macro(boolean, Closure) macro(CompilePhase, Closure)…
  40. 28 AST macros: variable substitution BlockStatement buildMD5MethodCode(FieldNode fieldNode) {
 VariableExpression

    fieldVar = GeneralUtils.varX(fieldNode.name)
 
 return macro(CompilePhase.SEMANTIC_ANALYSIS, true) {
 return java.security.MessageDigest
 .getInstance('MD5')
 .digest($v { fieldVar }.getBytes())
 .encodeHex()
 .toString()
 }
 }
  41. 28 AST macros: variable substitution BlockStatement buildMD5MethodCode(FieldNode fieldNode) {
 VariableExpression

    fieldVar = GeneralUtils.varX(fieldNode.name)
 
 return macro(CompilePhase.SEMANTIC_ANALYSIS, true) {
 return java.security.MessageDigest
 .getInstance('MD5')
 .digest($v { fieldVar }.getBytes())
 .encodeHex()
 .toString()
 }
 } Substitute this $v expression with 
 a variable from the context
  42. 29 AST macros: macro class @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class StatisticsASTTransformation

    extends AbstractASTTransformation {
 void visit(ASTNode[] nodes, SourceUnit source) {
 ClassNode classNode = (ClassNode) nodes[1]
 ClassNode templateClass = buildTemplateClass(classNode)
 
 templateClass.methods.each { MethodNode node ->
 classNode.addMethod(node)
 }
 }
 
 ClassNode buildTemplateClass(ClassNode reference) {
 def methodCount = constX(reference.methods.size())
 def fieldCount = constX(reference.fields.size())
 
 return new MacroClass() {
 class Statistics {
 java.lang.Integer getMethodCount() {
 return $v { methodCount }
 }
 
 java.lang.Integer getFieldCount() {
 return $v { fieldCount }
 }

  43. 29 AST macros: macro class @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class StatisticsASTTransformation

    extends AbstractASTTransformation {
 void visit(ASTNode[] nodes, SourceUnit source) {
 ClassNode classNode = (ClassNode) nodes[1]
 ClassNode templateClass = buildTemplateClass(classNode)
 
 templateClass.methods.each { MethodNode node ->
 classNode.addMethod(node)
 }
 }
 
 ClassNode buildTemplateClass(ClassNode reference) {
 def methodCount = constX(reference.methods.size())
 def fieldCount = constX(reference.fields.size())
 
 return new MacroClass() {
 class Statistics {
 java.lang.Integer getMethodCount() {
 return $v { methodCount }
 }
 
 java.lang.Integer getFieldCount() {
 return $v { fieldCount }
 }
 MacroClass not restricted to expressions & statements: you can generate methods & classes
  44. 30 Create custom macros withLock(lock) { // … } try

    { lock.lock() // … } finally { lock.unlock() }
  45. 30 Create custom macros withLock(lock) { // … } try

    { lock.lock() // … } finally { lock.unlock() } @Macro static Expression withLock( MacroContext ctx, Expression lockVar, ClosureExpression body) { callX(closureX( macro { try { $v { lockVar }.lock() $v { body }.call() } finally { $v { lockVar }.unlock() } } ), 'call') }
  46. 31 AST matchers @Retention(RetentionPolicy.SOURCE)
 @Target([ElementType.METHOD])
 @GroovyASTTransformationClass(["JokingASTTransformation"])
 @interface Joking { }

    @GroovyASTTransformation( phase = CompilePhase.INSTRUCTION_SELECTION)
 class JokingASTTransformation extends AbstractASTTransformation { 
 void visit(ASTNode[] nodes, SourceUnit source) {
 MethodNode methodNode = nodes[1]
 
 methodNode
 .getCode()
 .visit(
 new ConvertOnePlusOneToThree(source))
 }
 }
  47. 31 AST matchers @Retention(RetentionPolicy.SOURCE)
 @Target([ElementType.METHOD])
 @GroovyASTTransformationClass(["JokingASTTransformation"])
 @interface Joking { }

    @GroovyASTTransformation( phase = CompilePhase.INSTRUCTION_SELECTION)
 class JokingASTTransformation extends AbstractASTTransformation { 
 void visit(ASTNode[] nodes, SourceUnit source) {
 MethodNode methodNode = nodes[1]
 
 methodNode
 .getCode()
 .visit(
 new ConvertOnePlusOneToThree(source))
 }
 } class ConvertOnePlusOneToThree extends ClassCodeExpressionTransformer {
 SourceUnit sourceUnit
 
 ConvertOnePlusOneToThree(SourceUnit sourceUnit) {
 this.sourceUnit = sourceUnit
 }
 
 Expression transform(Expression exp) {
 Expression ref = macro { 1 + 1 }
 
 if (ASTMatcher.matches(ref, exp)) {
 return macro { 3 }
 }
 
 return super.transform(exp)
 }
 }
  48. 32 AST matchers void testTestingSumExpression() {
 use(ASTMatcher) {
 TwiceASTTransformation sample

    = new TwiceASTTransformation() 
 Expression referenceNode = macro {
 a + a
 }.withConstraints {
 placeholder 'a'
 }
 
 assert sample
 .sumExpression
 .matches(referenceNode)
 }
 }
  49. 32 AST matchers void testTestingSumExpression() {
 use(ASTMatcher) {
 TwiceASTTransformation sample

    = new TwiceASTTransformation() 
 Expression referenceNode = macro {
 a + a
 }.withConstraints {
 placeholder 'a'
 }
 
 assert sample
 .sumExpression
 .matches(referenceNode)
 }
 } Placeholders to match anything
  50. 34 Good ol’ do / while loops int i =

    0
 
 do {
 println i++
 } while (i < 10)
 
 assert i == 10
  51. 36 Identity comparison is back! class Person { String name

    }
 
 def p1 = new Person(name: 'Marion')
 def p2 = new Person(name: 'Érine')
 def p2copy = p2 
 
 assert p2 === p2copy
 assert p1 !== p2
  52. 37 !in and !instanceof if (!(1324 instanceof String)) {}
 if

    (1234 !instanceof String) {}
 
 assert !(3 in [])
 assert 3 !in []
  53. 38 Safe indexing ?[ ] List list = null
 assert

    list?[2] == null Map map = null
 assert map?['abc'] == null
  54. 39 Try with resources import java.util.zip.GZIPOutputStream
 
 def input =

    new File('./NOTICE')
 def output = new File('/tmp/zipped.zip')
 
 // Java-style
 try (
 FileInputStream fin = new FileInputStream(input);
 GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(output))
 ) {
 byte[] buffer = new byte[4096]
 int nread = 0
 while ((nread = fin.read(buffer)) != -1) {
 out.write(buffer, 0, nread)
 }
 }
  55. 40 Try with resources import java.util.zip.GZIPOutputStream
 
 def input =

    new File('./NOTICE')
 def output = new File('/tmp/zipped.zip')
 
 // Groovy-style :-)
 try (
 fin = new FileInputStream(input)
 out = new GZIPOutputStream(new FileOutputStream(output))
 ) { out << fin }
  56. 41 Lambdas! import static java.util.stream.Collectors.toList
 
 (1..10).forEach((it) -> { println

    it })
 
 assert (1..10).stream()
 .filter((it) -> it % 2 == 0)
 .map((it) -> it * 2)
 .collect(toList()) == [4, 8, 12, 16, 20]
  57. 42 Lambdas! // all the shapes (x, y) -> x

    + y
 (x, y) -> { x + y }
 (int x, int y) -> x + y
  58. 42 Lambdas! // all the shapes (x, y) -> x

    + y
 (x, y) -> { x + y }
 (int x, int y) -> x + y implicit return
  59. 42 Lambdas! // all the shapes (x, y) -> x

    + y
 (x, y) -> { x + y }
 (int x, int y) -> x + y def c = (int x, int y = 0) -> x + y
 assert c(1) == 1 implicit return
  60. 42 Lambdas! // all the shapes (x, y) -> x

    + y
 (x, y) -> { x + y }
 (int x, int y) -> x + y def c = (int x, int y = 0) -> x + y
 assert c(1) == 1 implicit return default argument
  61. 43 Method references — class refs import static java.util.stream.Collectors.toList
 


    // class::staticMethod
 assert ['1', '2', '3'] ==
 [1, 2, 3].stream()
 .map(String::valueOf)
 .collect(toList())
 
 // class::instanceMethod
 assert ['A', 'B', 'C'] ==
 ['a', 'b', 'c'].stream()
 .map(String::toUpperCase)
 .collect(toList())
  62. 44 Method references — instance refs class Name {
 String

    name
 char firstLetter() { name[0] }
 static cloneOf(Name n) { new Name(name: n.name + '2') }
 }
  63. 44 Method references — instance refs class Name {
 String

    name
 char firstLetter() { name[0] }
 static cloneOf(Name n) { new Name(name: n.name + '2') }
 } // instance::instanceMethod
 def fl = new Name(name: 'Guillaume')::firstLetter
 assert fl() == 'G'
 
 // instance::staticMethod
 def n = new Name(name: 'Marion')
 def clone = n::cloneOf
 assert clone(n).name == 'Marion2'
  64. 45 Method references — constructor refs def r = Random::new


    println r().nextInt(10) def arr2d = String[][]::new
 arr2d(2, 2) == [[null, null], [null, null]]
  65. 46 Method references — constructor refs import groovy.transform.Canonical
 
 @Canonical


    class Animal {
 String kind
 } def a = Animal::new
 assert a('lion').kind == 'lion'
 
 def c = Animal
 assert c::new('cat').kind == 'cat'
  66. 47 Elvis assignment import groovy.transform.ToString
 
 @ToString
 class Person {


    String name
 int age
 } def p = new Person(name: 'Érine')
 
 p.with {
 name = name ?: 'unknown'
 age = age ?: 4
 }
 
 assert p.toString() == 'Person(Érine, 4)'
  67. 47 Elvis assignment import groovy.transform.ToString
 
 @ToString
 class Person {


    String name
 int age
 } def p = new Person(name: 'Érine')
 
 p.with {
 name = name ?: 'unknown'
 age = age ?: 4
 }
 
 assert p.toString() == 'Person(Érine, 4)' p.with {
 name ?= 'unknown'
 age ?= 4
 }
  68. 48 JavaDoc comments saved import org.codehaus.groovy.control.*
 import static org.apache.groovy.parser.antlr4.AstBuilder.DOC_COMMENT
 


    def code = '''
 /**
 * class Foo
 * @author glaforge
 */
 class Foo {
 /** a method */
 void m(int i) {}
 }
 '''
  69. 48 JavaDoc comments saved import org.codehaus.groovy.control.*
 import static org.apache.groovy.parser.antlr4.AstBuilder.DOC_COMMENT
 


    def code = '''
 /**
 * class Foo
 * @author glaforge
 */
 class Foo {
 /** a method */
 void m(int i) {}
 }
 ''' def ast = new CompilationUnit().tap {
 addSource 'myscript.groovy', code
 compile Phases.SEMANTIC_ANALYSIS
 }.ast
 
 def cls = ast.classes[0] 
 assert cls.nodeMetaData[DOC_COMMENT] .contains('class Foo')
 assert cls.methods[0].nodeMetaData[DOC_COMMENT] .contains('a method')
  70. 49 @Groovydoc to get javadocs at runtime /** * @Groovydoc

    * Text content available at runtime
 */
 class A {}
  71. 49 @Groovydoc to get javadocs at runtime /** * @Groovydoc

    * Text content available at runtime
 */
 class A {} assert A.class.getAnnotation(Groovydoc).value() .contains('content available at runtime')