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

Apache Groovy keynote at G3 Summit

Apache Groovy keynote at G3 Summit

Reflecting on the Apache Groovy history and future, and covering the new features coming up in Groovy 2.4.x / 2.5 / 3.0

Guillaume Laforge

November 29, 2016
Tweet

More Decks by Guillaume Laforge

Other Decks in Technology

Transcript

  1. The Apache 
 Groovy Keynote! Guillaume Laforge
 @glaforge 
 PMC

    Chair of 
 Apache Groovy 
 
 Developer Advocate 
 Google Cloud Platform
  2. MOP

  3. 55 @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)'
  4. 55 @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
  5. 56 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.
  6. 57 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'
  7. 57 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() }
  8. 58 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) }
  9. 59 Prevent @TupleConstructor default ctors @TupleConstructor(defaults = true)
 class Person

    { String first, last
 int age
 } Generates only: Person(String first, String last, int age) { /*...*/ }
  10. 60 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)'
  11. 60 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
  12. 61 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()
  13. 61 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.
  14. 62 @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
 }
  15. 62 @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)'
  16. 63 @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'
  17. 63 @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
  18. 64 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
 }
 }
  19. 65 New @AutoImplement transformation @AutoImplement(exception = IOException)
 class MyWriter extends

    Writer { } @AutoImplement(exception = UnsupportedOperationException,
 message = 'Not supported by MyIterator')
 class MyIterator implements Iterator<String> { }
  20. 65 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 }
 }
  21. 66 :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
  22. 67 @Delegate on getters too class Person {
 String name

    @Delegate
 String getName() { name.reverse() } } def p = new Person(name: 'Erine') assert p.toUpperCase() == 'ENIRE'
  23. 68 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 }
  24. 68 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: 8) def xml = jaxbContext.marshal(p) assert jaxbContext.unmarshal(xml, Person) == p
  25. 69 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. 70 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. 70 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. 71 With… tap… assert new Person().with { name = 'Guillaume'

    age = 39 return it }.toString() == 'Person(Guillaume, 39)' @Canonical
 class Person {
 String name
 int age
 }
  29. 71 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. 73 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"}'
  31. 74 A customizable JSON serializer def generator = new JsonGenerator.Options()


    .addConverter(URL) { URL u, String key ->
 if (key == 'favoriteUrl') {
 u.getHost()
 } else {
 u
 }
 }
 .build()
  32. 74 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
  33. 74 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
  34. 77 AST macros: the old way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 @Override
 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)
 }
 }
  35. 77 AST macros: the old way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 @Override
 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
  36. 78 AST macros: the new way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 @Override
 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)
 }
 }
  37. 78 AST macros: the new way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 @Override
 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
  38. 78 AST macros: the new way @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
 class

    AddMethodASTTransformation extends AbstractASTTransformation {
 @Override
 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)…
  39. 80 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()
 }
 }
  40. 80 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 embedded $v value with 
 a variable from the context
  41. 81 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 }
 }

  42. 81 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
  43. 82 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))
 }
 }
  44. 82 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)
 }
 }
  45. 83 AST matchers void testTestingSumExpression() {
 use(ASTMatcher) {
 TwiceASTTransformation sample

    = new TwiceASTTransformation() 
 Expression referenceNode = macro {
 a + a
 }.withConstraints {
 placeholder 'a'
 }
 
 assert sample
 .sumExpression
 .matches(referenceNode)
 }
 }
  46. 83 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
  47. 85 Good ol’ do / while loops int i =

    0
 
 do {
 println i++
 } while (i < 10)
 
 assert i == 10
  48. 86 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
  49. 87 !in and !instanceof if (!(1324 instanceof String)) {}
 if

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

    list?[2] == null Map map = null
 assert map?['abc'] == null
  51. 89 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)
 }
 }
  52. 90 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 }
  53. 91 Try with resources import java.util.zip.GZIPOutputStream
 
 def input =

    new File('./NOTICE')
 def output = new File('/tmp/zipped.zip')
 
 // Groovy-style :-)
 
 
 
 out << fin
  54. 92 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]
  55. 93 Lambdas! // all the shapes (x, y) -> x

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

    + y
 (x, y) -> { x + y }
 (int x, int y) -> x + y implicit return
  57. 93 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
  58. 93 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
  59. 94 Method references — class refs import static java.util.stream.Collectors.toList
 


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

    name
 char firstLetter() { name[0] }
 static cloneOf(Name n) { new Name(name: n.name + '2') }
 }
  61. 95 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'
  62. 96 Method references — constructor refs def r = Random::new


    println r().nextInt(10) def arr2d = String[][]::new
 arr2d(2, 2) == [[null, null], [null, null]]
  63. 97 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'
  64. 98 Elvis assignment — Not yet there! import groovy.transform.ToString
 


    @ToString
 class Person {
 String name
 int age
 } def p = new Person(name: 'Érine')
 
 p.with {
 name = name ?: 'unknown'
 age = age ?: 3
 }
 
 assert p.toString() == 'Person(Érine, 3)'
  65. 98 Elvis assignment — Not yet there! import groovy.transform.ToString
 


    @ToString
 class Person {
 String name
 int age
 } def p = new Person(name: 'Érine')
 
 p.with {
 name = name ?: 'unknown'
 age = age ?: 3
 }
 
 assert p.toString() == 'Person(Érine, 3)' p.with {
 name ?= 'unknown'
 age ?= 3
 }
  66. 99 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) {}
 }
 '''
  67. 99 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 'methodM.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')
  68. 101 Picture credits The elephant in the room http://www.elephantsinthelivingroom.org/backgrounds/elephant-in-room.jpg Road

    https://pixabay.com/fr/route-marquage-de-route-rue-miles-166543/ Prague https://pixabay.com/fr/prague-r%C3%A9publique-tch%C3%A8que-ville-1168302/ Wise men http://img03.deviantart.net/4431/i/2012/044/f/6/three_wise_men_color_by_csoro-d4pmlv2.jpg Rocket lift-off https://pixabay.com/fr/fus%C3%A9e-ses-9-lancement-cap-canaveral-1245696/ Java beans https://pixabay.com/fr/grains-de-caf%C3%A9-caf%C3%A9-caf%C3%A9ine-boire-167001/ Ring http://www.zastavki.com/pictures/originals/2013/Movies__037523_.jpg Letter G https://pixabay.com/fr/typographie-geschtaltung-fontes-1069409/ Grammar https://pixabay.com/fr/grammaire-abc-dictionnaire-mots-390029/
  69. 102 Picture credits Copy and paste https://d262ilb51hltx0.cloudfront.net/max/2000/1*sglxoCq-p9gbefsUfGaNlg.jpeg A new hope

    http://static.dolimg.com/lucas/movies/starwars/starwars_epi4_01-4f17cf46f1dd.jpg Snails https://pixabay.com/fr/escargots-course-gast%C3%A9ropodes-1753611/ The revenant https://static.independent.co.uk/s3fs-public/thumbnails/image/2016/02/18/16/The-Revenant-Leo.jpg Broken window https://pixabay.com/fr/endroits-perdus-rompu-fen%C3%AAtre-verre-1798639/ Ducklings https://pixabay.com/fr/canard-canetons-les-b%C3%A9b%C3%A9s-caneton-207344/ Jigsaw https://pixabay.com/fr/pi%C3%A8ces-du-puzzle-mix-les-mains-592798/ Bee Gees http://cdn-mag.itcher.com/mag/wp-content/uploads/2015/06/BeeGees.jpg Buddha https://pixabay.com/fr/bouddha-le-bouddhisme-statue-525883/
  70. 103 Picture credits Cousin https://pixabay.com/fr/cousins-jouer-l-ext%C3%A9rieur-jouets-1587250/ Dollar https://pixabay.com/fr/dollar-billet-de-banque-%C3%A9tats-unis-1161782/ Flower bloom https://pixabay.com/fr/fleur-de-printemps-arbre-nature-289844/

    Baby shoes https://pixabay.com/fr/chaussures-grossesse-enfant-505471/ Grails https://isimbolinellacomunicazione.files.wordpress.com/2013/02/calice_du_sacre_tau.jpg Eclipse https://pixabay.com/fr/%C3%A9clipses-solaire-soleil-434338/ Baseball catch up https://pixabay.com/fr/baseball-lecteur-jeu-jouer-1504968/ Dogs https://pixabay.com/fr/chiens-matraques-jouer-morsure-708354/ Niche / kennel https://pixabay.com/fr/chien-niche-cage-peluche-1199846/
  71. 104 Picture credits Fist punch https://pixabay.com/fr/poing-coup-puissance-wrestling-1561157/ Throne http://static.srcdn.com/wp-content/uploads/GoT-Players-Iron-Throne-Feature.jpg Migrating birds

    https://pixabay.com/fr/troupeau-flocage-oies-migrer-vol-378676/ Trash https://pixabay.com/fr/poubelle-garbage-seau-vert-1111448/ MOP http://i.huffpost.com/gen/1176457/images/o-CLEAN-MOP-facebook.jpg Backward snow gliding https://pixabay.com/fr/enfant-fille-bob-rouler-sur-1214231/ Rabbit & tortoise https://media.senscritique.com/media/000008120110/1200/Le_Lievre_et_la_Tortue.jpg Car lights https://pixabay.com/fr/autoroute-photo-de-nuit-feux-nuit-393492/ Agility http://wallpoper.com/images/00/15/50/09/lifestyle-female-dynamic-movement-of-women_00155009.jpg
  72. 105 Picture credits Watch complication https://gildedholdings.files.wordpress.com/2014/10/lange-grand-complication-uhrwerk-movement-1-neu.jpg Speed meter https://pixabay.com/fr/odom%C3%A8tre-1810107/ Swiss

    army knife https://pixabay.com/fr/arm%C3%A9e-de-terre-lame-compact-coup%C3%A9-15563/ Punk https://pixabay.com/fr/jeune-jeune-fille-jeunesse-punk-175504/ Jason http://static.comicvine.com/uploads/original/10/103184/3150852-2994250-0447705879-jason.jpg Creep http://orig11.deviantart.net/f87a/f/2012/092/1/5/creep_by_holavengoaflotar-d4urypg.jpg Jammed wires https://www.exchangewire.com/wp-content/uploads/2016/09/7670055210_ceb0c9ef9a_b.jpg