Groovy 面向对象

本章节主要讲述Groovy编程语言的面向对象知识点。

1. 类型

1.1. 基本类型

Groovy同样支持Java语言定义的基本数据类型:

  • 整型: byte (8 bit), short (16 bit), int (32 bit) and long (64 bit)

  • 浮点型: float (32 bit) and double (64 bit)

  • 布尔型 (exactly true or false)

  • 字符型 (16 bit, 可作为数据类型使用, 表示一个 UTF-16 编码)

Groovy将原始字段和变量声明为原语并将其存储为原始类型,因为它对所有内容都使用对象,所以它会自动包装对原语的引用。就像Java一样,它使用的包装器

表1. 基础封装
基础类型封装类

boolean

Boolean

char

Character

short

Short

int

Integer

long

Long

float

Float

double

Double

int数据类型的示例:

class Foo {
    static int i
}

assert Foo.class.getDeclaredField('i').type == int.class
assert Foo.i.class != int.class && Foo.i.class == Integer.class

现在你可能担心的是,每次在使用原始类型想着的算数运算付的时候,都会引入封装的消耗。但这些都不是问题,因为Groovy会将你的运算符进行编译并替换。换句话说,当你调用带原始类型的Java方法的时候,Groovy会自动解封数据类型为原始类型,并且会自动封装原始类型的返回值。总之注意与Java方法的解决的异同就可以了。

1.2. 类


Groovy中类比Java中的类要简单,而且与Java兼容。它们可以有方法、字段和属性(类似JavaBeans属性,但少了些模板数据)。类和类成员都有相同的修饰符(public,protected,private, static等等),有些在Groovy会有一些不同的简写。

  • Groovy的类和在Java中的副本最主要的区别是:

  • 不带修饰符的类或成员都会自动加上public修饰符。

  • 不带修饰符的字段会自动转为属性,会减少很多不必要的代码,getter和setter方法可以不用明确写出。

  • 类不再需要和源码文件相同的名称定义,但多数情况下还是推荐的。

  • 一个源文件可能会包含一个或多个类(但如果一个文件中的代码不止是类的话,它会被认为是脚本)。脚本就是包括一些特殊约定的类,并且会和它们的源文件拥有相同的名称(所以在脚本中不要写和源文件相同名称的类定义)

The following code presents an example class.

下面是一个类的简单示例:

class Person {                        // 定义一个名称是Person的类

    String name                       // 定义一个属性名为name的字符串字段
    Integer age

    def increaseAge(Integer years) {  // 方法定义
        this.age += years
    }
}

1.2.1. 普通类

普通类是最高级的具体类,它们可以被实例化,不受其他任何类和脚本的限制。就是说,它们只能是公有的public。类通过调用它们的构造函数被实例化,使用new关键字。

def p = new Person()

1.2.2. 内部类(Inner classes)

内部类被定义在其他类的内部。封装类可以像平常一样使用内部类,换句话说,一个内部类可以访问封装类的类成员,私有成员也可以访问,其他类不允许访问内部类:

class Outer {
    private String privateStr

    def callInnerMethod() {
        new Inner().methodA()          // 初始化内部类,并调用方法。
    }

    class Inner {                      // 定义一个内部类在它的封装类中
        def methodA() {
            println "${privateStr}."   // 内部类可以访问封装类中的私有成员变量
        }
    }
}

为什么要使用内部类?

  • 内部类可以进一步提高封装质量,其他类不能访问,可以净化包和工作空间。

  • 通过将只有一个类访问的类进行分组,达到一个很好的组织。

  • 内部类靠近使用它的类,提高了代码的可维护性。

在很多例子中,内部类是一个接口的实现,实现的方法正好是被外部类用到的。下面是关于线程使用的代码:

class Outer2 {
    private String privateStr = 'some string'

    def startThread() {
        new Thread(new Inner2()).start()
    }

    class Inner2 implements Runnable {
        void run() {
            println "${privateStr}."
        }
    }
}

注意到,内部类Inner2只是为外部类Outer2提供了方法的实现,那么在这个例子中,匿名类可以帮助消除不必要的代码。

从Groovy3.0.0开始,Java语法中非静态内部类的初始化已经得到实现:

class Computer {
    class Cpu {
        int coreNumber

        Cpu(int coreNumber) {
            this.coreNumber = coreNumber
        }
    }
}
匿名内部类

下面是使用匿名内部类来实现上面的线程代码:

class Outer3 {
    private String privateStr = 'some string'

    def startThread() {
        new Thread(new Runnable() {       // new Inner2()被new Runnable()替换
            void run() {
                println "${privateStr}."
            }
        }).start()                        // start方法被正常调用
    }
}

这样就没有必要多定义一个类了。

1.2.3. 抽象类

抽象类表示一个能用的概念,因此,它们不能被实例化,只能被子类实例化。它们的成员包括字段/属性,抽象或具体的方法。抽象方法没有方法体,但必须被子类实现。

abstract class Abstract {            // 抽象类有abstract关键字修饰
    String name

    abstract def abstractMethod()    // 抽象方法必须用abstract关键字修饰

    def concreteMethod() {
        println 'concrete'
    }
}

抽象类通常与接口相比较,但是抽象类和接口有至少两个重要的不同点。第一,抽象类可以包含字段/属性和具体方法,接口只能包含抽象方法。第二,一个类可以实现多个接口,但只能实现一个抽象类。

1.3. 接口

接口定义一个类需要遵守的规则。接口只需要定义一系列的方法,而不需要去实现它们。

interface Greeter {         // 声明接口必须使用interface关键字
  void greet(String name)   // 只需要定义方法名,不需要实现方法
}

Methods of an interface are always public. It is an error to use protected or private methods in interfaces:

接口的方法都是公有的,如果修饰一个方法为protected或private,编译时会有异常。

interface Greeter {
   protected void greet(String name)  
}

一个类实现接口的时候,必须实现接口中定义的方法。

class SystemGreeter implements Greeter {   // 类实现接口的时候必须使用implements关键字
    void greet(String name) {              // 必须实现greet方法
        println "Hello $name"
    }
}

def greeter = new SystemGreeter()    
assert greeter instanceof Greeter          // 任意SystemGreeter类的实例,都是Greeter接口的实例。

接口可以继承另一个接口:

interface ExtendedGreeter extends Greeter {   // 接口继承接口的时候要使用extends关键字。
  void sayBye(String name) 
}

值得注意的是,一个类要明确是一个接口的实例,例如,下面代码中的类定义了一个greet方法,几样在Greeter接口也声明了一个,但是这个类并没有实现这个接口。

class DefaultGreeter {
    void greet(String name) { println "Hello" }
}

greeter = new DefaultGreeter()
assert !(greeter instanceof Greeter)

换句话说,Groovy没有结构类型,所以很容易在运行时期创建一个对象接口实例,使用强转运算符as:

greeter = new DefaultGreeter()       // 创建一个没有实现接口的实例
coerced = greeter as Greeter         // 在运行时,强转实例为接口Greeter
assert coerced instanceof Greeter    // 强转的实例实现了Greeter接口。

你可以看到有两个明显不同的对象,一个是源对象,DefaultGreeter类的实例,没有实现接口;另一个是委托实现了接口的实例对象。

Groovy接口不支持Java8接口的默认实现。如果你在寻找类似的实现,traits和接口相近,但是允许默认实现。

1.4. 构造函数

构造函数是一个特殊的方法,它是用来实例化一个带有特定状态的对象。和普通方法一样,可能有多个构造函数,只要每个构造函数都有一个唯一的类型签名。如果一个对象在实例化的时候没有参数,就需要一个无参的构造函数,如果没有提供,那么会由Groovy编译器提供一个默认的构造函数。

Groovy支持两种调用方式:

  • 定位参数的使用和Java构造函数的使用类似。

  • 命名参数允许你在调用构造函数的时候指定参数的名称。

1.4.1. 定位参数

通过定位参数创建一个对象,各自的类需要声明一个或多个构造函数。在多个构造函数中,每一个都必须有唯一的类型签名。构造函数也能通过 groovy.transform.TupleConstructor 注解添加。

通常,至少需要一个构造函数,一个类只能通过调用构造函数来实例化。值得注意的是,带有命名参数的类和定位参数不同,如果想通过命名参数构造实例,那么一个类就需要有一个无参的构造函数,或是提供一个Map类型参数的构造函数。

使用声明的构造函数有三种方式,第一个就是普通的Java方式,使用new关键字。第二种依赖于强转运算符as,来强转参数数组。第三种就是通过强转赋值的方式构造实例。

class PersonConstructor {
    String name
    Integer age

    PersonConstructor(name, age) {                 // 构造函数声明
        this.name = name
        this.age = age
    }
}

def person1 = new PersonConstructor('Marie', 1)   // new关键的方式构造实例
def person2 = ['Marie', 2] as PersonConstructor   // 使用强转关键字as构造实例
PersonConstructor person3 = ['Marie', 3]          // 使用强转赋值的方式构造实例

1.4.2. 命名参数

如果没有构造函数,或是声明了一个无参的构造函数,就可以通过键值对的map类型方式来构造一个实例。这就很适用于参数很多的构造函数,而且使用传统的定位参数的方式,就需要声明所有参数需要的构造函数。Having a constructor where the first (and perhaps only) argument is a Map argument is also supported - such a constructor may also be added using the groovy.transform.MapConstructor annotation.

class PersonWOConstructor {
    String name
    Integer age
    // 没有构造函数声明
}

def person4 = new PersonWOConstructor()     // 无参数实例化
def person5 = new PersonWOConstructor(name: 'Marie')     // 定位参数实例化
def person6 = new PersonWOConstructor(age: 1)            // 命名参数实例化
def person7 = new PersonWOConstructor(name: 'Marie', age: 2)   // name和age参数的实例化

这里需要重点强调,虽然命名参数的方式给予了构造函数很大的便利,但在写代码的时候就需要特别注意参数名和值必须正确。如果定位参数也同时声明了,那么定位参数的优先级更高。

注意:

  • 上面的例子没有提供构造函数,你也可以提供一个无参的构造函数,或是一个带有Map参数的构造函数,通常情况下就只有一个参数。

  • 当没有提供构造函数,或是提供了一个无参的构造函数时,Groovy会把提供的参数调用setter方法进行赋值。

  • 当第一个参数为Map时,Groovy会将所有命名参数都组合成一个Map(无序)并提供给第一个参数Map,如果你的属性在最后声明了,这是一个很好的方式(这样参数会设置到构造参数中,而不是使用setter进行处理)

  • You can support both named and positional construction by supply both positional constructors as well as a no-arg or Map constructor.

  • You can support hybrid construction by having a constructor where the first argument is a Map but there are also additional positional parameters. Use this style with caution.

1.5. 方法

Groovy的方法和其他语言的方法相似,一些独特性会在下个章节提到。

1.5.1. 方法定义

定义方法可以通过返回类型,或是通过def关键字来定义一个无返回类型的方法。方法可以接收任意参数,可以不用明确的声明类型。Java修饰符也可以正常使用,如果没有提供显示的修饰符,那么该方法就是公有的public。

Groovy的方法有返回值,如果没有return提供,那么最后一行将会被返回。例如,下面的方法都没有使用return关键字。

def someMethod() { 'method called' }                            // 没有返回类型,没有参数的方法           
String anotherMethod() { 'another method called' }              // 明确的返回类型,没有参数的方法
def thirdMethod(param1) { "$param1 passed" }                    // 没有类型的参数
static String fourthMethod(String param1) { "$param1 passed" }  // 字符串类型参数的静态方法

1.5.2. 命名参数

和构造函数一样,普通的方法也可以使用命名参数。约定方法的第一个参数为Map,在方法体中,参数值可以通过map.key的方式访问。如果方法只有一个单独的Map参数,所有提供的参数必须要有名称。

def foo(Map args) { "${args.name}: ${args.age}" } foo(name: 'Marie', age: 1)
命名参数和定位参数混合使用

命名参数可以和定位参数混合使用,同样约定,第一个参数为Map,其他参数其它定位参数放在后面,定位参数必须的有序的,命名参数,可以放在任意位置。程序会自动将所有键值对组合。

def foo(Map args, Integer number) {
 "${args.name}: ${args.age}, and the number is ${number}" 
} 
foo(name: 'Marie', age: 1, 23)   // 带额外整形参数的方法调用
foo(23, name: 'Marie', age: 1)   // 改变参数顺序的方法调用

如果没有把Map放在第一个参数位,命名参数将不可以再使用,只能使用Map类型的参数来代替,否则会报groovy.lang.MissingMethodException 异常:

def foo(Integer number, Map args) {
 "${args.name}: ${args.age}, and the number is ${number}" 
} 
foo(name: 'Marie', age: 1, 23)   // 使用命名参数会报异常 groovy.lang.MissingMethodException

使用明确的Map参数,可能避免异常:

def foo(Integer number, Map args) {
 "${args.name}: ${args.age}, and the number is ${number}" 
} 
foo(23, [name: 'Marie', age: 1])  // 使用Map参数避免异常的发生

虽然Groovy允许命名参数和定位参数混用,但这样会引起不必要的混淆,所以要小心使用。

1.5.3. 默认参数

默认参数是可选的,如果没有提供默认参数,方法会分配一个默认值。

def foo(String par1, Integer par2 = 1) {
 [name: par1, age: par2] 
} 
assert foo('Marie').age == 1

定义默认参数值后,方法不会再强制分配默认值了。

1.5.4. 可变参数

Groovy支持可变数量参数的方法。就像这样:def foo(p1, …, pn, T… args)。这里foo默认支持n个参数,但也支持超过n个参数的未指定数量的其他参数。

def foo(Object... args) { args.length } 
assert foo() == 0 
assert foo(1) == 1 
assert foo(1, 2) == 2

这个例子定义了一个方法foo,能接收任意数量的参数,也包括没有参数。args.length会返回给定参数的数量。Groovy允许T[]替换T,这意味着Groovy将任何以数组作为最后一个参数的方法视为可以接受可变数量参数的方法。

def foo(Object[] args) { args.length } 
assert foo() == 0 
assert foo(1) == 1 
assert foo(1, 2) == 2

如果可变参数的方法接收了一个null作为可变参数,那么args就是一个null,而不是一个可变参数的数组。

def foo(Object... args) { args } 
assert foo(null) == null

如果可变参数的方法接收了一个数组参数,那么参数args会被这个数组替代,而不是一个数组中包含这个数组。如果传入了两个数组参数,那么就是正常的可变参数,一个数组中,包含两个数组。

def foo(Object... args) { args } 
Integer[] ints = [1, 2] 
assert foo(ints) == [1, 2]

另一个非常重要的地方就是可变参数的重载。假如有方法重载,Groovy会选择一个明确的方法。例如一个方法包含了可变参数类型T,另一个方法包含了一个参数类型T,那么第二个方法拥有更高的优先级。

def foo(Object... args) { 1 } 
def foo(Object x) { 2 } 
assert foo() == 1 
assert foo(1) == 2 
assert foo(1, 2) == 1

1.5.5. 方法选择规则

动态Groovy支持多重派发(multiple dispatch)(也就是多态)。当调用一个方法的时候,实际上方法调用是基于运行时方法的参数类型动态决定的。第一,要考虑方法名和参数数量(也要考虑可变参数量),还有第一个参数的参数类型。想想以下定义方法:

def method(Object o1, Object o2) { 'o/o' } 
def method(Integer i, String  s) { 'i/s' } 
def method(String  s, Integer i) { 's/i' }

或许和预期的一样,传入一个字符串和一个整型参数,会调用第三个方法。

assert method('foo', 42) == 's/i'

有趣的是在编译期参数类型都是未知的,参数可能会被声明为Object(一组objects),Java将决定在所有情况下都选择方法(Object,Object)变量(除非使用了cast),但如下面的示例所示,Groovy使用运行时类型并将调用我们的每个方法一次(通常不需要cast):

List<List<Object>> pairs = [['foo', 1], [2, 'bar'], [3, 4]] 
assert pairs.collect { a, b -> method(a, b) } == ['s/i', 'i/s', 'o/o']

对于我们的三个方法调用中的前两个,调用中的每一个,都找到了参数类型的精确匹配。对于第三次调用,找不到方法(IntegerInteger)的精确匹配,但方法(ObjectObject)仍然有效,将被选中。

然后,方法选择就是从具有兼容参数类型的有效候选方法中找到最合适的方法。因此,方法(Object,Object)对于前两个调用也是有效的,但与类型完全匹配的变体的匹配程度不同。为了确定最接近的匹配,运行时有一个实际参数类型与声明的参数类型之间的距离的概念,并尝试最小化所有参数之间的总距离。

下表说明了影响距离计算的一些因素。

AspectExample

直接实现的接口比继承层次结构更接近。

Given these interface and method definitions:

interface I1 {}
interface I2 extends I1 {}
interface I3 {}
class Clazz implements I3, I2 {}

def method(I1 i1) { 'I1' }
def method(I3 i3) { 'I3' }

The directly implemented interface will match:

assert method(new Clazz()) == 'I3'

对象数组优先于对象。

def method(Object[] arg) { 'array' } 
def method(Object arg) { 'object' } 
assert method([] as Object[]) == 'array'

非可变参数的方法具有更高的优先级。

def method(String s, Object... vargs) { 'vararg' } 
def method(String s) { 'non-vararg' } 
assert method('foo') == 'non-vararg'

如果两个方法都带有可变参数,那么使用可变参数最少的那个方法具有更高的优先级。

def method(String s, Object... vargs) { 'two vargs' } 
def method(String s, Integer i, Object... vargs) { 'one varg' } 
assert method('foo', 35, new Date()) == 'one varg'

接口比父类的优先级更高。

interface I {}
class Base {}
class Child extends Base implements I {}

def method(Base b) { 'superclass' }
def method(I i) { 'interface' }

assert method(new Child()) == 'interface'

对于原始参数类型,首选相同或稍大的参数类型。

def method(Long l) { 'Long' } 
def method(Short s) { 'Short' } 
def method(BigInteger bi) { 'BigInteger' } 
assert method(35) == 'Long'

如果两个变量具有完全相同的距离,则这将被视为不明确,并将导致运行时异常:

def method(Date d, Object o) { 'd/o' } 
def method(Object o, String s) { 'o/s' } 
def ex = shouldFail {
     println method(new Date(), 'baz') 
} 
assert ex.message.contains('Ambiguous method overloading')

强转用于选择所需的方法:

assert method(new Date(), (Object)'baz') == 'd/o' 
assert method((Object)new Date(), 'baz') == 'o/s'

1.5.6. 异常声明

Groovy自动允许您像对待未检查的异常一样对待已检查的异常。这意味着您不需要声明方法可能引发的任何选中异常,如以下示例所示,如果找不到文件,该示例可能引发FileNotFoundException:

def badRead() { new File('doesNotExist.txt').text } 
shouldFail(FileNotFoundException) {
    badRead() 
}

也不需要将上一个示例中对badRead方法的调用包围在try/catch块中—尽管您可以随意这样做。

如果您希望声明代码可能抛出的任何异常(选中或其他),您可以这样做。添加异常不会改变任何其他Groovy代码的代码使用方式,但可以看作是代码的人类读者的文档。异常将成为字节码中方法声明的一部分,因此如果您的代码可能是从Java调用的,那么包含它们可能会很有用。使用显式checked异常声明如以下示例所示:

def badRead() throws FileNotFoundException { new File('doesNotExist.txt').text } 
shouldFail(FileNotFoundException) { 
    badRead() 
}

1.6. 字段和属性

1.6.1. 字段

字段是具有以下特性的类或特征的成员:

  • 强制访问修饰符(public、protected或private)

  • 一个或多个可选修饰符(静态、最终、同步)

  • 可选类型

  • 强制性名称

class Data {
    private int id                            // 私有字段id,类型int
    protected String description              // 保护字段,description,字符串类型
    public static final boolean DEBUG = false // public static final 字段DEBUG,布尔类型
}

字段可以在声明时直接初始化:

class Data { 
    private String id = IDGenerator.next()  // IDGenerator.next() 初始化字段id
}

可以省略字段的类型声明。但是,这被认为是一种不好的做法,一般来说,对字段使用强类型是一个好主意:

class BadPractice {
    private mapping    // 字段mapping未声明类型
}
class GoodPractice {
    private Map<String,String> mapping    // 字段mapping有一个强类型
}

如果以后要使用可选的类型检查,那么这两者之间的区别很重要。作为记录类设计的一种方式,它也很重要。但是,在某些情况下,例如脚本编写或如果您希望依赖duck类型,则省略该类型可能会很有用。

1.6.2. 属性

属性是类的外部可见功能。Java中的典型方法是遵循JavaBeans规范中概述的约定,而不是仅仅使用公共字段来表示这些特性(这提供了更有限的抽象,并且会限制重构的可能性),即,使用私有支持字段和getter/setter的组合来表示属性。Groovy遵循这些相同的约定,但提供了一种更简单的方法来定义属性。可以使用以下方法定义特性:

  • 不存在的访问修饰符(没有公共的、受保护的或私有的)

  • 一个或多个可选修饰符(静态、最终、同步)

  • 可选类型

  • 强制性名称

Groovy随后将适当地生成getter/setter。例如:

class Person {
    String name   // 创建一个 private String name 字段,getName和setName方法 的备份
    int age       // 创建一个 private int age 字段,getName和setName方法 的备份
}

如果一个属性被声明为final,那么setter方法将不会被创建:

class Person {
    final String name               // 定义只读String属性name
    final int age                   // 定义只读int类型属性age
    Person(String name, int age) {  
        this.name = name            // 赋值name
        this.age = age              // 赋值age
    }
}

属性按名称访问,并将透明地调用getter或setter,除非代码位于定义属性的类中:

class Person {
    String name
    void name(String name) {
        this.name = "Wonder$name"     // this.name将会直接访问字段name,因为是在类内访问的。
    }
    String wonder() {
        this.name                     // 访问name 也是一样直接访问的。
    }
}
def p = new Person()
p.name = 'Marge'                      // 在类外写字段name,需要隐式的调用setName方法赋值
assert p.name == 'Marge'              // 类外访问字段name,需要隐式的调用getName方法
p.name('Marge')                       // 调用name方法,这样会直接访问name字段
assert p.wonder() == 'WonderMarge'    // 调用wonder方法,这样会直接读取字段name

值得注意的是,这种直接访问backing字段的行为是为了在定义属性的类中使用属性访问语法时防止堆栈溢出。

由于实例的元属性字段,可以列出类的属性:

class Person {
    String name
    int age
}
def p = new Person()
assert p.properties.keySet().containsAll(['name','age'])

按照惯例,如果有遵循javabean规范的getter或setter,Groovy将识别属性,即使没有支持字段。例如:

class PseudoProperties {
    // a pseudo property "name"
    void setName(String name) {}
    String getName() {}

    // a pseudo read-only property "age"
    int getAge() { 42 }

    // a pseudo write-only property "groovy"
    void setGroovy(boolean groovy) {  }
}
def p = new PseudoProperties()
p.name = 'Foo'
assert p.age == 42
p.groovy = true

writing p.name is allowed because there is a pseudo-property name

reading p.age is allowed because there is a pseudo-readonly property age

writing p.groovy is allowed because there is a pseudo-writeonly property groovy

这种语法糖是许多用Groovy编写的dsl的核心。

属性命名约定

通常建议属性名的前两个字母为小写,对于多字属性,使用驼峰写法。在这些情况下,生成的getter和setter将具有一个名称,其形式是将属性名称大写并添加get或set前缀(或者可选地为boolean getter添加“is”)。因此,getLength是length属性的getter,setFirstName是firstName属性的setter。isEmpty可能是名为empty的属性的getter方法名称。

以大写字母开头的属性名将具有仅添加前缀的getter/setter。因此,允许使用Foo属性,即使它没有遵循推荐的命名约定。对于这个属性,访问器方法将是setFoo和getFoo。这样做的结果是不允许同时具有foo和foo属性,因为它们将具有相同的命名访问器方法。

JavaBeans规范为通常可能是首字母缩写的属性提供了一个特例。如果属性名的前两个字母是大写,则不执行大小写(或者更重要的是,如果从访问器方法名生成属性名,则不执行去大写)。因此,getURL将是URL属性的getter。

由于JavaBeans规范中特殊的“首字母缩写处理”属性命名逻辑,与属性名的转换是不对称的。这导致了一些奇怪的边缘情况。Groovy采用了一种命名约定,它避免了一种模糊性,这种模糊性可能看起来有点奇怪,但在Groovy设计时很流行,而且由于历史原因(到目前为止)一直存在。Groovy查看属性名的第二个字母。如果是资本,则该财产被视为首字母缩写形式的财产之一,不进行资本化,否则进行正常资本化。尽管我们从不推荐它,但它确实允许您拥有看似“重复命名”的属性,例如,您可以拥有aProp和aProp,或pNAME和pNAME。getter将分别是getaProp和getaProp,以及getpNAME和getpNAME。

1.7. 注解

1.7.1. 注解定义

注释是一种专用于注释代码元素的特殊接口。注释是一种类型,其superinterface是注释接口。注释的声明方式与接口非常相似,使用@interface关键字:

@interface SomeAnnotation {}

注释可以以不带正文和可选默认值的方法的形式定义成员。可能的成员类型限于:

  • 原始类型

  • 字符串类型

  • 枚举

  • 其他注解

  • 上面类型的数组形式

例如:

@interface SomeAnnotation {
    String value()             // 一个注解定义了字符串类型的成员变量value
}
@interface SomeAnnotation {
    String value() default 'something'   // 含有默认值
}
@interface SomeAnnotation {
    int step()                 // int类型的成员变量step
}
@interface SomeAnnotation {
    Class appliesTo()          // 类类型
}
@interface SomeAnnotation {}
@interface SomeAnnotations {
    SomeAnnotation[] value()   // 包含其他注解的数组类型
}
enum DayOfWeek { mon, tue, wed, thu, fri, sat, sun }
@interface Scheduled {
    DayOfWeek dayOfWeek()      // 枚举类型
}

与Java语言不同,在Groovy中,注释可以用来改变语言的语义。尤其是AST转换,它将基于注释生成代码。

1.7.2. 注解位置

注释可以应用于代码的各个元素:

@SomeAnnotation         // 适用于方法
void someMethod() {
    // ...
}

@SomeAnnotation         // 适用于类
class SomeClass {}

@SomeAnnotation String var    // 适用于属性

为了限制可以应用注释的范围,有必要使用目标注释在注释定义中声明注释。例如,下面是如何声明注释可以应用于类或方法:

import java.lang.annotation.ElementType
import java.lang.annotation.Target

@Target([ElementType.METHOD, ElementType.TYPE])    // @Target注解限定了注解的范围
@interface SomeAnnotation {}                       // @SomeAnnotation 注解只允许就用于Type或是方法

可用的目标列表: ElementType enumeration.

Groovy不支持TYPE_PARAMETER 和 TYPE_USE 类型元素(在Java8中介绍的)

1.7.3. 注解成员值

当使用注解的时候,需要设置所有没有默认值的成员变量值,例如:

@interface Page {
    int statusCode()
}

@Page(statusCode=404)
void notFound() {
    // ...
}

如果注解中只有一个没有默认值的成员,并且在声明注解成员值的时候,只设置这个没有默认值的成员,那么可以忽略value=

@interface Page {
    String value()
    int statusCode() default 200
}

@Page(value='/home')               // 可以忽略statusCode,因为它有默认值,不能忽略value
void home() {
    // ...
}

@Page('/users')                    // 因为value是唯一没有默认值的永久性成员,所以可以忽略value=
void userList() {
    // ...
}

@Page(value='error',statusCode=404)  // 如果其他属性statusCode也设置值了,那么value就不能忽略value=
void notFound() {
    // ...
}

1.7.4 保留策略

注解的可见性取决于其保留策略。注解的保留策略是使用保留批注设置的:

import java.lang.annotation.Retention 
import java.lang.annotation.RetentionPolicy 
@Retention(RetentionPolicy.SOURCE)     // @Retention 注解解释了@SomeAnnotation注解
@interface SomeAnnotation {}           // @SomeAnnotation将拥有源码保留

RetentionPolicy枚举中提供了可能的保留目标和说明的列表。选择通常取决于您希望注释在编译时还是运行时可见。

1.7.5. 闭包注释参数

Groovy中注释的一个有趣特性是可以使用闭包作为注释值。因此,注释可以与各种各样的表达式一起使用,并且仍然具有IDE支持。例如,设想一个框架,在这个框架中,您希望执行一些基于环境约束的方法,比如JDK版本或OS。可以编写以下代码:

class Tasks {
    Set result = []
    void alwaysExecuted() {
        result << 1
    }
    @OnlyIf({ jdk>=6 })
    void supportedOnlyInJDK6() {
        result << 'JDK 6'
    }
    @OnlyIf({ jdk>=7 && windows })
    void requiresJDK7AndWindows() {
        result << 'JDK 7 Windows'
    }
}

For the @OnlyIf annotation to accept a Closure as an argument, you only have to declare the value as a Class:

@Retention(RetentionPolicy.RUNTIME) 
@interface OnlyIf { Class value()  }

To complete the example, let’s write a sample runner that would use that information:

class Runner {
    static <T> T run(Class<T> taskClass) {
        def tasks = taskClass.newInstance()
        def params = [jdk:6, windows: false]
        tasks.class.declaredMethods.each { m ->
            if (Modifier.isPublic(m.modifiers) && m.parameterTypes.length == 0) {
                def onlyIf = m.getAnnotation(OnlyIf)
                if (onlyIf) {
                    Closure cl = onlyIf.value().newInstance(tasks,tasks)
                    cl.delegate = params
                    if (cl()) {
                        m.invoke(tasks)
                    }
                } else {
                    m.invoke(tasks)
                }
            }
        }
        tasks
    }
}

create a new instance of the class passed as an argument (the task class)

emulate an environment which is JDK 6 and not Windows

iterate on all declared methods of the task class

if the method is public and takes no-argument

try to find the @OnlyIf annotation

if it is found get the value and create a new Closure out of it

set the delegate of the closure to our environment variable

call the closure, which is the annotation closure. It will return a boolean

if it is true, call the method

if the method is not annotated with @OnlyIf, execute the method anyway

after that, return the task object

Then the runner can be used this way:

def tasks = Runner.run(Tasks) 
assert tasks.result == [1, 'JDK 6'] as Set

1.7.6. 元注解

声明元注解

Meta-annotations, also known as annotation aliases are annotations that are replaced at compile time by other annotations (one meta-annotation is an alias for one or more annotations). Meta-annotations can be used to reduce the size of code involving multiple annotations.

Let’s start with a simple example. Imagine you have the @Service and @Transactional annotations and that you want to annotate a class with both:

@Service @Transactional class MyTransactionalService {}

Given the multiplication of annotations that you could add to the same class, a meta-annotation could help by reducing the two annotations with a single one having the very same semantics. For example, we might want to write this instead:

@TransactionalService  class MyTransactionalService {}

@TransactionalService is a meta-annotation

A meta-annotation is declared as a regular annotation but annotated with @AnnotationCollector and the list of annotations it is collecting. In our case, the @TransactionalService annotation can be written:

import groovy.transform.AnnotationCollector @Service  @Transactional  @AnnotationCollector  @interface TransactionalService { }

annotate the meta-annotation with @Service

annotate the meta-annotation with @Transactional

annotate the meta-annotation with @AnnotationCollector
元注解的行为

Groovy supports both precompiled and source form meta-annotations. This means that your meta-annotation may be precompiled, or you can have it in the same source tree as the one you are currently compiling.

INFO: Meta-annotations are a Groovy-only feature. There is no chance for you to annotate a Java class with a meta-annotation and hope it will do the same as in Groovy. Likewise, you cannot write a meta-annotation in Java: both the meta-annotation definition and usage have to be Groovy code. But you can happily collect Java annotations and Groovy annotations within your meta-annotation.

When the Groovy compiler encounters a class annotated with a meta-annotation, it replaces it with the collected annotations. So, in our previous example, it will replace @TransactionalService with @Transactional and @Service:

def annotations = MyTransactionalService.annotations*.annotationType() assert (Service in annotations) assert (Transactional in annotations)

The conversion from a meta-annotation to the collected annotations is performed during the semantic analysis compilation phase. 

In addition to replacing the alias with the collected annotations, a meta-annotation is capable of processing them, including arguments.

元注解的参数

Meta-annotations can collect annotations which have parameters. To illustrate this, we will imagine two annotations, each of them accepting one argument:

@Timeout(after=3600) @Dangerous(type='explosive')

And suppose that you want create a meta-annotation named @Explosive:

@Timeout(after=3600) @Dangerous(type='explosive') @AnnotationCollector public @interface Explosive {}

By default, when the annotations are replaced, they will get the annotation parameter values as they were defined in the alias. More interesting, the meta-annotation supports overriding specific values:

@Explosive(after=0)  class Bomb {}

the after value provided as a parameter to @Explosive overrides the one defined in the @Timeout annotation

If two annotations define the same parameter name, the default processor will copy the annotation value to all annotations that accept this parameter:

@Retention(RetentionPolicy.RUNTIME) public @interface Foo { String value()  } @Retention(RetentionPolicy.RUNTIME) public @interface Bar { String value()  } @Foo @Bar @AnnotationCollector public @interface FooBar {}  @Foo('a') @Bar('b') class Bob {}  assert Bob.getAnnotation(Foo).value() == 'a'  println Bob.getAnnotation(Bar).value() == 'b'  @FooBar('a') class Joe {}  assert Joe.getAnnotation(Foo).value() == 'a'  println Joe.getAnnotation(Bar).value() == 'a'

the @Foo annotation defines the value member of type String

the @Bar annotation also defines the value member of type String

the @FooBar meta-annotation aggregates @Foo and @Bar

class Bob is annotated with @Foo and @Bar

the value of the @Foo annotation on Bob is a

while the value of the @Bar annotation on Bob is b

class Joe is annotated with @FooBar

then the value of the @Foo annotation on Joe is a

and the value of the @Bar annotation on Joe is also a

In the second case, the meta-annotation value was copied in both @Foo and @Bar annotations.


It is a compile time error if the collected annotations define the same members with incompatible types. For example if on the previous example @Foo defined a value of type String but @Bar defined a value of type int.

It is however possible to customize the behavior of meta-annotations and describe how collected annotations are expanded. We’ll look at how to do that shortly but first there is an advanced processing option to cover.

处理重复注解

The @AnnotationCollector annotation supports a mode parameter which can be used to alter how the default processor handles annotation replacement in the presence of duplicate annotations.

INFO: Custom processors (discussed next) may or may not support this parameter.

As an example, suppose you create a meta-annotation containing the @ToString annotation and then place your meta-annotation on a class that already has an explicit @ToString annotation. Should this be an error? Should both annotations be applied? Does one take priority over the other? There is no correct answer. In some scenarios it might be quite appropriate for any of these answers to be correct. So, rather than trying to preempt one correct way to handle the duplicate annotation issue, Groovy let’s you write your own custom meta-annotation processors (covered next) and let’s you write whatever checking logic you like within AST transforms - which are a frequent target for aggregating. Having said that, by simply setting the mode, a number of commonly expected scenarios are handled automatically for you within any extra coding. The behavior of the mode parameter is determined by the AnnotationCollectorMode enum value chosen and is summarized in the following table.

Mode

Description

DUPLICATE

Annotations from the annotation collection will always be inserted. After all transforms have been run, it will be an error if multiple annotations (excluding those with SOURCE retention) exist.

PREFER_COLLECTOR

Annotations from the collector will be added and any existing annotations with the same name will be removed.

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.

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.

自定义注解处理器

A custom annotation processor will let you choose how to expand a meta-annotation into collected annotations. The behaviour of the meta-annotation is, in this case, totally up to you. To do this, you must:

  • create a meta-annotation processor, extending AnnotationCollectorTransform

  • declare the processor to be used in the meta-annotation declaration

To illustrate this, we are going to explore how the meta-annotation @CompileDynamic is implemented.

@CompileDynamic is a meta-annotation that expands itself to @CompileStatic(TypeCheckingMode.SKIP). The problem is that the default meta annotation processor doesn’t support enums and the annotation value TypeCheckingMode.SKIP is one.

The naive implementation here would not work:

@CompileStatic(TypeCheckingMode.SKIP) @AnnotationCollector public @interface CompileDynamic {}

Instead, we will define it like this:

@AnnotationCollector(processor = "org.codehaus.groovy.transform.CompileDynamicProcessor") public @interface CompileDynamic { }

The first thing you may notice is that our interface is no longer annotated with @CompileStatic. The reason for this is that we rely on the processor parameter instead, that references a class which will generate the annotation.

Here is how the custom processor is implemented:

CompileDynamicProcessor.groovy

@CompileStatic  class CompileDynamicProcessor extends AnnotationCollectorTransform {  private static final ClassNode CS_NODE = ClassHelper.make(CompileStatic)  private static final ClassNode TC_NODE = ClassHelper.make(TypeCheckingMode)  List<AnnotationNode> visit(AnnotationNode collector,  AnnotationNode aliasAnnotationUsage,  AnnotatedNode aliasAnnotated,  SourceUnit source) {  def node = new AnnotationNode(CS_NODE)  def enumRef = new PropertyExpression( new ClassExpression(TC_NODE), "SKIP")          node.addMember("value", enumRef)  Collections.singletonList(node)  } }

our custom processor is written in Groovy, and for better compilation performance, we use static compilation

the custom processor has to extend AnnotationCollectorTransform

create a class node representing the @CompileStatic annotation type

create a class node representing the TypeCheckingMode enum type

collector is the @AnnotationCollector node found in the meta-annotation. Usually unused.

aliasAnnotationUsage is the meta-annotation being expanded, here it is @CompileDynamic

aliasAnnotated is the node being annotated with the meta-annotation

sourceUnit is the SourceUnit being compiled

we create a new annotation node for @CompileStatic

we create an expression equivalent to TypeCheckingMode.SKIP

we add that expression to the annotation node, which is now @CompileStatic(TypeCheckingMode.SKIP)

return the generated annotation

In the example, the visit method is the only method which has to be overridden. It is meant to return a list of annotation nodes that will be added to the node annotated with the meta-annotation. In this example, we return a single one corresponding to @CompileStatic(TypeCheckingMode.SKIP).

1.8. 继承

(TBD)

1.9.泛型

(TBD)

2. Traits

Traits are a structural construct of the language which allows:

  • composition of behaviors

  • runtime implementation of interfaces

  • behavior overriding

  • compatibility with static type checking/compilation

They can be seen as interfaces carrying both default implementations and state. A trait is defined using the trait keyword:

trait FlyingAbility {  String fly() { "I'm flying!" }  }

declaration of a trait

declaration of a method inside a trait

Then it can be used like a normal interface using the implements keyword:

class Bird implements FlyingAbility {}  def b = new Bird()  assert b.fly() == "I'm flying!"

Adds the trait FlyingAbility to the Bird class capabilities

instantiate a new Bird

the Bird class automatically gets the behavior of the FlyingAbility trait

Traits allow a wide range of capabilities, from simple composition to testing, which are described thoroughly in this section.

2.1. 方法

2.1.1. 公共方法

Declaring a method in a trait can be done like any regular method in a class:

trait FlyingAbility {  String fly() { "I'm flying!" }  }

declaration of a trait

declaration of a method inside a trait

2.1.2. 抽象方法

In addition, traits may declare abstract methods too, which therefore need to be implemented in the class implementing the trait:

trait Greetable { abstract String name()  String greeting() { "Hello, ${name()}!" }  }

implementing class will have to declare the name method

can be mixed with a concrete method

Then the trait can be used like this:

class Person implements Greetable {  String name() { 'Bob' }  } def p = new Person() assert p.greeting() == 'Hello, Bob!'

implement the trait Greetable

since name was abstract, it is required to implement it

then greeting can be called

2.1.3. 私有方法

Traits may also define private methods. Those methods will not appear in the trait contract interface:

trait Greeter { private String greetingMessage() {  'Hello from a private method!' } String greet() { def m = greetingMessage()          println m         m     } } class GreetingMachine implements Greeter {}  def g = new GreetingMachine() assert g.greet() == "Hello from a private method!"  try { assert g.greetingMessage()  } catch (MissingMethodException e) {     println "greetingMessage is private in trait" }

define a private method greetingMessage in the trait

the public greet message calls greetingMessage by default

create a class implementing the trait

greet can be called

but not greetingMessage

Traits only support public and private methods. Neither protected nor package private scopes are supported.

2.1.4. Final 方法

If we have a class implementing a trait, conceptually implementations from the trait methods are "inherited" into the class. But, in reality, there is no base class containing such implementations. Rather, they are woven directly into the class. A final modifier on a method just indicates what the modifier will be for the woven method. While it would likely be considered bad style to inherit and override or multiply inherit methods with the same signature but a mix of final and non-final variants, Groovy doesn’t prohibit this scenario. Normal method selection applies and the modifier used will be determined from the resulting method. You might consider creating a base class which implements the desired trait(s) if you want trait implementation methods that can’t be overridden.

2.2. this的含义

this represents the implementing instance. Think of a trait as a superclass. This means that when you write:

trait Introspector { def whoAmI() { this } } class Foo implements Introspector {} def foo = new Foo()

then calling:

foo.whoAmI()

will return the same instance:

assert foo.whoAmI().is(foo)

2.3. 接口

Traits may implement interfaces, in which case the interfaces are declared using the implements keyword:

interface Named {  String name() } trait Greetable implements Named {  String greeting() { "Hello, ${name()}!" } } class Person implements Greetable {  String name() { 'Bob' }  } def p = new Person() assert p.greeting() == 'Hello, Bob!'  assert p instanceof Named  assert p instanceof Greetable

declaration of a normal interface

add Named to the list of implemented interfaces

declare a class that implements the Greetable trait

implement the missing name method

the greeting implementation comes from the trait

make sure Person implements the Named interface

make sure Person implements the Greetable trait

2.4. 属性

A trait may define properties, like in the following example:

trait Named { String name                              } class Person implements Named {}  def p = new Person(name: 'Bob')  assert p.name == 'Bob'  assert p.getName() == 'Bob'

declare a property name inside a trait

declare a class which implements the trait

the property is automatically made visible

it can be accessed using the regular property accessor

or using the regular getter syntax

2.5. 字段

2.5.1. 私有字段

Since traits allow the use of private methods, it can also be interesting to use private fields to store state. Traits will let you do that:

trait Counter { private int count = 0  int count() { count += 1; count }  } class Foo implements Counter {}  def f = new Foo() assert f.count() == 1  assert f.count() == 2

declare a private field count inside a trait

declare a public method count that increments the counter and returns it

declare a class that implements the Counter trait

the count method can use the private field to keep state

This is a major difference with Java 8 virtual extension methods. While virtual extension methods do not carry state, traits can. Moreover, traits in Groovy are supported starting with Java 6, because their implementation does not rely on virtual extension methods. This means that even if a trait can be seen from a Java class as a regular interface, that interface will not have default methods, only abstract ones.

2.5.2. 公共字段

Public fields work the same way as private fields, but in order to avoid the diamond problem, field names are remapped in the implementing class:

trait Named { public String name                       } class Person implements Named {}  def p = new Person()  p.Named__name = 'Bob'

declare a public field inside the trait

declare a class implementing the trait

create an instance of that class

the public field is available, but renamed

The name of the field depends on the fully qualified name of the trait. All dots (.) in package are replaced with an underscore (_), and the final name includes a double underscore. So if the type of the field is String, the name of the package is my.package, the name of the trait is Foo and the name of the field is bar, in the implementing class, the public field will appear as:

String my_package_Foo__bar

While traits support public fields, it is not recommended to use them and considered as a bad practice.

2.6. Composition of behaviors

Traits can be used to implement multiple inheritance in a controlled way. For example, we can have the following traits:

trait FlyingAbility {  String fly() { "I'm flying!" }  } trait SpeakingAbility { String speak() { "I'm speaking!" } }

And a class implementing both traits:

class Duck implements FlyingAbility, SpeakingAbility {}  def d = new Duck()  assert d.fly() == "I'm flying!"  assert d.speak() == "I'm speaking!"

the Duck class implements both FlyingAbility and SpeakingAbility

creates a new instance of Duck

we can call the method fly from FlyingAbility

but also the method speak from SpeakingAbility

Traits encourage the reuse of capabilities among objects, and the creation of new classes by the composition of existing behavior.

2.7. Overriding default methods

Traits provide default implementations for methods, but it is possible to override them in the implementing class. For example, we can slightly change the example above, by having a duck which quacks:

class Duck implements FlyingAbility, SpeakingAbility { String quack() { "Quack!" }  String speak() { quack() }  } def d = new Duck() assert d.fly() == "I'm flying!"  assert d.quack() == "Quack!"  assert d.speak() == "Quack!"

define a method specific to Duck, named quack

override the default implementation of speak so that we use quack instead

the duck is still flying, from the default implementation

quack comes from the Duck class

speak no longer uses the default implementation from SpeakingAbility

2.8. Extending traits

2.8.1. Simple inheritance

Traits may extend another trait, in which case you must use the extends keyword:

trait Named { String name                                      } trait Polite extends Named {  String introduce() { "Hello, I am $name" }  } class Person implements Polite {} def p = new Person(name: 'Alice')  assert p.introduce() == 'Hello, I am Alice'

the Named trait defines a single name property

the Polite trait extends the Named trait

Polite adds a new method which has access to the name property of the super-trait

the name property is visible from the Person class implementing Polite

as is the introduce method

2.8.2. Multiple inheritance

Alternatively, a trait may extend multiple traits. In that case, all super traits must be declared in the implements clause:

trait WithId {  Long id } trait WithName {  String name } trait Identified implements WithId, WithName {}

WithId trait defines the id property

WithName trait defines the name property

Identified is a trait which inherits both WithId and WithName

2.9. Duck typing and traits

2.9.1. Dynamic code

Traits can call any dynamic code, like a normal Groovy class. This means that you can, in the body of a method, call methods which are supposed to exist in an implementing class, without having to explicitly declare them in an interface. This means that traits are fully compatible with duck typing:

trait SpeakingDuck { String speak() { quack() }  } class Duck implements SpeakingDuck { String methodMissing(String name, args) { "${name.capitalize()}!"  } } def d = new Duck() assert d.speak() == 'Quack!'

the SpeakingDuck expects the quack method to be defined

the Duck class does implement the method using methodMissing

calling the speak method triggers a call to quack which is handled by methodMissing

2.9.2. Dynamic methods in a trait

It is also possible for a trait to implement MOP methods like methodMissing or propertyMissing, in which case implementing classes will inherit the behavior from the trait, like in this example:

trait DynamicObject {  private Map props = [:] def methodMissing(String name, args) {         name.toUpperCase() } def propertyMissing(String prop) {         props[prop] } void setProperty(String prop, Object value) {         props[prop] = value     } } class Dynamic implements DynamicObject { String existingProperty = 'ok'  String existingMethod() { 'ok' }  } def d = new Dynamic() assert d.existingProperty == 'ok'  assert d.foo == null  d.foo = 'bar'  assert d.foo == 'bar'  assert d.existingMethod() == 'ok'  assert d.someMethod() == 'SOMEMETHOD'

create a trait implementing several MOP methods

the Dynamic class defines a property

the Dynamic class defines a method

calling an existing property will call the method from Dynamic

calling an non-existing property will call the method from the trait

will call setProperty defined on the trait

will call getProperty defined on the trait

calling an existing method on Dynamic

but calling a non existing method thanks to the trait methodMissing

2.10. Multiple inheritance conflicts

2.10.1. Default conflict resolution

It is possible for a class to implement multiple traits. If some trait defines a method with the same signature as a method in another trait, we have a conflict:

trait A { String exec() { 'A' }  } trait B { String exec() { 'B' }  } class C implements A,B {}

trait A defines a method named exec returning a String

trait B defines the very same method

class C implements both traits

In this case, the default behavior is that the method from the last declared trait in the implements clause wins. Here, B is declared after A so the method from B will be picked up:

def c = new C() assert c.exec() == 'B'

2.10.2. User conflict resolution

In case this behavior is not the one you want, you can explicitly choose which method to call using the Trait.super.foo syntax. In the example above, we can ensure the method from trait A is invoked by writing this:

class C implements A,B { String exec() { A.super.exec() }  } def c = new C() assert c.exec() == 'A'

explicit call of exec from the trait A

calls the version from A instead of using the default resolution, which would be the one from B

2.11. Runtime implementation of traits

2.11.1. Implementing a trait at runtime

Groovy also supports implementing traits dynamically at runtime. It allows you to "decorate" an existing object using a trait. As an example, let’s start with this trait and the following class:

trait Extra { String extra() { "I'm an extra method" }  } class Something {  String doSomething() { 'Something' }  }

the Extra trait defines an extra method

the Something class does not implement the Extra trait

Something only defines a method doSomething

Then if we do:

def s = new Something() s.extra()

the call to extra would fail because Something is not implementing Extra. It is possible to do it at runtime with the following syntax:

def s = new Something() as Extra  s.extra()  s.doSomething()

use of the as keyword to coerce an object to a trait at runtime

then extra can be called on the object

and doSomething is still callable

When coercing an object to a trait, the result of the operation is not the same instance. It is guaranteed that the coerced object will implement both the trait and the interfaces that the original object implements, but the result will not be an instance of the original class.

2.11.2. Implementing multiple traits at once

Should you need to implement several traits at once, you can use the withTraits method instead of the as keyword:

trait A { void methodFromA() {} } trait B { void methodFromB() {} } class C {} def c = new C() c.methodFromA()  c.methodFromB()  def d = c.withTraits A, B            d.methodFromA()  d.methodFromB()

call to methodFromA will fail because C doesn’t implement A

call to methodFromB will fail because C doesn’t implement B

withTrait will wrap c into something which implements A and B

methodFromA will now pass because d implements A

methodFromB will now pass because d also implements B

When coercing an object to multiple traits, the result of the operation is not the same instance. It is guaranteed that the coerced object will implement both the traits and the interfaces that the original object implements, but the result will not be an instance of the original class.

2.12. Chaining behavior

Groovy supports the concept of stackable traits. The idea is to delegate from one trait to the other if the current trait is not capable of handling a message. To illustrate this, let’s imagine a message handler interface like this:

interface MessageHandler { void on(String message, Map payload) }

Then you can compose a message handler by applying small behaviors. For example, let’s define a default handler in the form of a trait:

trait DefaultHandler implements MessageHandler { void on(String message, Map payload) {         println "Received $message with payload $payload" } }

Then any class can inherit the behavior of the default handler by implementing the trait:

class SimpleHandler implements DefaultHandler {}

Now what if you want to log all messages, in addition to the default handler? One option is to write this:

class SimpleHandlerWithLogging implements DefaultHandler { void on(String message, Map payload) {          println "Seeing $message with payload $payload"  DefaultHandler.super.on(message, payload)  } }

explicitly implement the on method

perform logging

continue by delegating to the DefaultHandler trait

This works but this approach has drawbacks:

  1. the logging logic is bound to a "concrete" handler

  2. we have an explicit reference to DefaultHandler in the on method, meaning that if we happen to change the trait that our class implements, code will be broken

As an alternative, we can write another trait which responsibility is limited to logging:

trait LoggingHandler implements MessageHandler {  void on(String message, Map payload) {         println "Seeing $message with payload $payload"  super.on(message, payload)  } }

the logging handler is itself a handler

prints the message it receives

then super makes it delegate the call to the next trait in the chain

Then our class can be rewritten as this:

class HandlerWithLogger implements DefaultHandler, LoggingHandler {} def loggingHandler = new HandlerWithLogger() loggingHandler.on('test logging', [:])

which will print:

Seeing test logging with payload [:] Received test logging with payload [:]

As the priority rules imply that LoggerHandler wins because it is declared last, then a call to on will use the implementation from LoggingHandler. But the latter has a call to super, which means the next trait in the chain. Here, the next trait is DefaultHandler so both will be called:

The interest of this approach becomes more evident if we add a third handler, which is responsible for handling messages that start with say:

trait SayHandler implements MessageHandler { void on(String message, Map payload) { if (message.startsWith("say")) {              println "I say ${message - 'say'}!" } else { super.on(message, payload)  } } }

a handler specific precondition

if the precondition is not met, pass the message to the next handler in the chain

Then our final handler looks like this:

class Handler implements DefaultHandler, SayHandler, LoggingHandler {} def h = new Handler() h.on('foo', [:]) h.on('sayHello', [:])

Which means:

  • messages will first go through the logging handler

  • the logging handler calls super which will delegate to the next handler, which is the SayHandler

  • if the message starts with say, then the handler consumes the message

  • if not, the say handler delegates to the next handler in the chain

This approach is very powerful because it allows you to write handlers that do not know each other and yet let you combine them in the order you want. For example, if we execute the code, it will print:

Seeing foo with payload [:] Received foo with payload [:] Seeing sayHello with payload [:] I say Hello!

but if we move the logging handler to be the second one in the chain, the output is different:

class AlternateHandler implements DefaultHandler, LoggingHandler, SayHandler {} h = new AlternateHandler() h.on('foo', [:]) h.on('sayHello', [:])

prints:

Seeing foo with payload [:] Received foo with payload [:] I say Hello!

The reason is that now, since the SayHandler consumes the message without calling super, the logging handler is not called anymore.

2.12.1. Semantics of super inside a trait

If a class implements multiple traits and a call to an unqualified super is found, then:

  1. if the class implements another trait, the call delegates to the next trait in the chain

  2. if there isn’t any trait left in the chain, super refers to the super class of the implementing class (this)

For example, it is possible to decorate final classes thanks to this behavior:

trait Filtering {  StringBuilder append(String str) {  def subst = str.replace('o','')  super.append(subst)  } String toString() { super.toString() }  } def sb = new StringBuilder().withTraits Filtering  sb.append('Groovy') assert sb.toString() == 'Grvy'

define a trait named Filtering, supposed to be applied on a StringBuilder at runtime

redefine the append method

remove all 'o’s from the string

then delegate to super

in case toString is called, delegate to super.toString

runtime implementation of the Filtering trait on a StringBuilder instance

the string which has been appended no longer contains the letter o

In this example, when super.append is encountered, there is no other trait implemented by the target object, so the method which is called is the original append method, that is to say the one from StringBuilder. The same trick is used for toString, so that the string representation of the proxy object which is generated delegates to the toString of the StringBuilder instance.

2.13. Advanced features

2.13.1. SAM type coercion

If a trait defines a single abstract method, it is candidate for SAM (Single Abstract Method) type coercion. For example, imagine the following trait:

trait Greeter { String greet() { "Hello $name" }  abstract String getName()  }

the greet method is not abstract and calls the abstract method getName

getName is an abstract method

Since getName is the single abstract method in the Greeter trait, you can write:

Greeter greeter = { 'Alice' }

the closure "becomes" the implementation of the getName single abstract method

or even:

void greet(Greeter g) { println g.greet() }  greet { 'Alice' }

the greet method accepts the SAM type Greeter as parameter

we can call it directly with a closure

2.13.2. Differences with Java 8 default methods

In Java 8, interfaces can have default implementations of methods. If a class implements an interface and does not provide an implementation for a default method, then the implementation from the interface is chosen. Traits behave the same but with a major difference: the implementation from the trait is always used if the class declares the trait in its interface list and that it doesn’t provide an implementation even if a super class does.

This feature can be used to compose behaviors in an very precise way, in case you want to override the behavior of an already implemented method.

To illustrate the concept, let’s start with this simple example:

import groovy.test.GroovyTestCase import groovy.transform.CompileStatic import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer import org.codehaus.groovy.control.customizers.ImportCustomizer class SomeTest extends GroovyTestCase { def config     def shell     void setup() {         config = new CompilerConfiguration()         shell = new GroovyShell(config) } void testSomething() { assert shell.evaluate('1+1') == 2 } void otherTest() { /* ... */ } }

In this example, we create a simple test case which uses two properties (config and shell) and uses those in multiple test methods. Now imagine that you want to test the same, but with another distinct compiler configuration. One option is to create a subclass of SomeTest:

class AnotherTest extends SomeTest { void setup() {         config = new CompilerConfiguration()         config.addCompilationCustomizers( ... )         shell = new GroovyShell(config) } }

It works, but what if you have actually multiple test classes, and that you want to test the new configuration for all those test classes? Then you would have to create a distinct subclass for each test class:

class YetAnotherTest extends SomeTest { void setup() {         config = new CompilerConfiguration()         config.addCompilationCustomizers( ... )         shell = new GroovyShell(config) } }

Then what you see is that the setup method of both tests is the same. The idea, then, is to create a trait:

trait MyTestSupport { void setup() {         config = new CompilerConfiguration()         config.addCompilationCustomizers( new ASTTransformationCustomizer(CompileStatic) )         shell = new GroovyShell(config) } }

Then use it in the subclasses:

class AnotherTest extends SomeTest implements MyTestSupport {} class YetAnotherTest extends SomeTest2 implements MyTestSupport {} ...

It would allow us to dramatically reduce the boilerplate code, and reduces the risk of forgetting to change the setup code in case we decide to change it. Even if setup is already implemented in the super class, since the test class declares the trait in its interface list, the behavior will be borrowed from the trait implementation!

This feature is in particular useful when you don’t have access to the super class source code. It can be used to mock methods or force a particular implementation of a method in a subclass. It lets you refactor your code to keep the overridden logic in a single trait and inherit a new behavior just by implementing it. The alternative, of course, is to override the method in every place you would have used the new code.


It’s worth noting that if you use runtime traits, the methods from the trait are always preferred to those of the proxied object:
class Person { String name                                          } trait Bob { String getName() { 'Bob' }  } def p = new Person(name: 'Alice') assert p.name == 'Alice'  def p2 = p as Bob  assert p2.name == 'Bob'

the Person class defines a name property which results in a getName method

Bob is a trait which defines getName as returning Bob

the default object will return Alice

p2 coerces p into Bob at runtime

getName returns Bob because getName is taken from the trait

Again, don’t forget that dynamic trait coercion returns a distinct object which only implements the original interfaces, as well as the traits.

2.14. Differences with mixins

There are several conceptual differences with mixins, as they are available in Groovy. Note that we are talking about runtime mixins, not the @Mixin annotation which is deprecated in favour of traits.

First of all, methods defined in a trait are visible in bytecode:

  • internally, the trait is represented as an interface (without default or static methods) and several helper classes

  • this means that an object implementing a trait effectively implements an interface

  • those methods are visible from Java

  • they are compatible with type checking and static compilation

Methods added through a mixin are, on the contrary, only visible at runtime:

class A { String methodFromA() { 'A' } }  class B { String methodFromB() { 'B' } }  A.metaClass.mixin B                              def o = new A() assert o.methodFromA() == 'A'  assert o.methodFromB() == 'B'  assert o instanceof A                            assert !(o instanceof B)

class A defines methodFromA

class B defines methodFromB

mixin B into A

we can call methodFromA

we can also call methodFromB

the object is an instance of A

but it’s not an instanceof B

The last point is actually a very important and illustrates a place where mixins have an advantage over traits: the instances are not modified, so if you mixin some class into another, there isn’t a third class generated, and methods which respond to A will continue responding to A even if mixed in.

2.15. Static methods, properties and fields


The following instructions are subject to caution. Static member support is work in progress and still experimental. The information below is valid for 3.0.7 only.

It is possible to define static methods in a trait, but it comes with numerous limitations:

  • Traits with static methods cannot be compiled statically or type checked. All static methods, properties and field are accessed dynamically (it’s a limitation from the JVM).

  • Static methods do not appear within the generated interfaces for each trait.

  • The trait is interpreted as a template for the implementing class, which means that each implementing class will get its own static methods, properties and fields. So a static member declared on a trait doesn’t belong to the Trait, but to its implementing class.

  • You should typically not mix static and instance methods of the same signature. The normal rules for applying traits apply (including multiple inheritance conflict resolution). If the method chosen is static but some implemented trait has an instance variant, a compilation error will occur. If the method chosen is the instance variant, the static variant will be ignored (the behavior is similar to static methods in Java interfaces for this case).

Let’s start with a simple example:

trait TestHelper { public static boolean CALLED = false  static void init() {          CALLED = true  } } class Foo implements TestHelper {} Foo.init()  assert Foo.TestHelper__CALLED

the static field is declared in the trait

a static method is also declared in the trait

the static field is updated within the trait

a static method init is made available to the implementing class

the static field is remapped to avoid the diamond issue

As usual, it is not recommended to use public fields. Anyway, should you want this, you must understand that the following code would fail:

Foo.CALLED = true

because there is no static field CALLED defined on the trait itself. Likewise, if you have two distinct implementing classes, each one gets a distinct static field:

class Bar implements TestHelper {}  class Baz implements TestHelper {}  Bar.init()  assert Bar.TestHelper__CALLED  assert !Baz.TestHelper__CALLED

class Bar implements the trait

class Baz also implements the trait

init is only called on Bar

the static field CALLED on Bar is updated

but the static field CALLED on Baz is not, because it is distinct

2.16. Inheritance of state gotchas

We have seen that traits are stateful. It is possible for a trait to define fields or properties, but when a class implements a trait, it gets those fields/properties on a per-trait basis. So consider the following example:

trait IntCouple { int x = 1 int y = 2 int sum() { x+y } }

The trait defines two properties, x and y, as well as a sum method. Now let’s create a class which implements the trait:

class BaseElem implements IntCouple { int f() { sum() } } def base = new BaseElem() assert base.f() == 3

The result of calling f is 3, because f delegates to sum in the trait, which has state. But what if we write this instead?

class Elem implements IntCouple { int x = 3  int y = 4  int f() { sum() }  } def elem = new Elem()

Override property x

Override property y

Call sum from trait

If you call elem.f(), what is the expected output? Actually it is:

assert elem.f() == 3

The reason is that the sum method accesses the fields of the trait. So it is using the x and y values defined in the trait. If you want to use the values from the implementing class, then you need to dereference fields by using getters and setters, like in this last example:

trait IntCouple { int x = 1 int y = 2 int sum() { getX()+getY() } } class Elem implements IntCouple { int x = 3 int y = 4 int f() { sum() } } def elem = new Elem() assert elem.f() == 7

2.17. Self types

2.17.1. Type constraints on traits

Sometimes you will want to write a trait that can only be applied to some type. For example, you may want to apply a trait on a class that extends another class which is beyond your control, and still be able to call those methods. To illustrate this, let’s start with this example:

class CommunicationService { static void sendMessage(String from, String to, String message) {          println "$from sent [$message] to $to" } } class Device { String id }  trait Communicating { void sendMessage(Device to, String message) { CommunicationService.sendMessage(id, to.id, message)  } } class MyDevice extends Device implements Communicating {}  def bob = new MyDevice(id:'Bob') def alice = new MyDevice(id:'Alice') bob.sendMessage(alice,'secret')

A Service class, beyond your control (in a library, …) defines a sendMessage method

A Device class, beyond your control (in a library, …)

Defines a communicating trait for devices that can call the service

Defines MyDevice as a communicating device

The method from the trait is called, and id is resolved

It is clear, here, that the Communicating trait can only apply to Device. However, there’s no explicit contract to indicate that, because traits cannot extend classes. However, the code compiles and runs perfectly fine, because id in the trait method will be resolved dynamically. The problem is that there is nothing that prevents the trait from being applied to any class which is not a Device. Any class which has an id would work, while any class that does not have an id property would cause a runtime error.

The problem is even more complex if you want to enable type checking or apply @CompileStatic on the trait: because the trait knows nothing about itself being a Device, the type checker will complain saying that it does not find the id property.

One possibility is to explicitly add a getId method in the trait, but it would not solve all issues. What if a method requires this as a parameter, and actually requires it to be a Device?

class SecurityService { static void check(Device d) { if (d.id==null) throw new SecurityException() } }

If you want to be able to call this in the trait, then you will explicitly need to cast this into a Device. This can quickly become unreadable with explicit casts to this everywhere.

2.17.2. The @SelfType annotation

In order to make this contract explicit, and to make the type checker aware of the type of itself, Groovy provides a @SelfType annotation that will:

  • let you declare the types that a class that implements this trait must inherit or implement

  • throw a compile time error if those type constraints are not satisfied

So in our previous example, we can fix the trait using the @groovy.transform.SelfType annotation:

@SelfType(Device) @CompileStatic trait Communicating { void sendMessage(Device to, String message) { SecurityService.check(this) CommunicationService.sendMessage(id, to.id, message) } }

Now if you try to implement this trait on a class that is not a device, a compile-time error will occur:

class MyDevice implements Communicating {} // forgot to extend Device

The error will be:

class 'MyDevice' implements trait 'Communicating' but does not extend self type class 'Device'

In conclusion, self types are a powerful way of declaring constraints on traits without having to declare the contract directly in the trait or having to use casts everywhere, maintaining separation of concerns as tight as it should be.

2.18. Limitations

2.18.1. Compatibility with AST transformations


Traits are not officially compatible with AST transformations. Some of them, like @CompileStatic will be applied on the trait itself (not on implementing classes), while others will apply on both the implementing class and the trait. There is absolutely no guarantee that an AST transformation will run on a trait as it does on a regular class, so use it at your own risk!

2.18.2. Prefix and postfix operations

Within traits, prefix and postfix operations are not allowed if they update a field of the trait:

trait Counting { int x     void inc() {         x++  } void dec() { --x                              } } class Counter implements Counting {} def c = new Counter() c.inc()

x is defined within the trait, postfix increment is not allowed

x is defined within the trait, prefix decrement is not allowed

A workaround is to use the += operator instead.