本章介绍 Groovy 编程语言的面向对象方面。

1. 类型

1.1. 原始类型

Groovy 支持与 Java 语言规范 定义相同的原始类型。

  • 整型:`byte` (8 位), `short` (16 位), `int` (32 位) 和 `long` (64 位)

  • 浮点型:`float` (32 位) 和 `double` (64 位)

  • 布尔类型:`boolean` (`true` 或 `false` 之一)

  • 字符类型:`char` (16 位,可用作数字类型,表示 UTF-16 码)

与 Java 类似,当需要与任何原始类型对应的对象时,Groovy 会使用相应的包装类。

表 1. 原始类型包装器
原始类型 包装类

boolean

Boolean

char

字符

short

Short

int

Integer

long

Long

float

Float

double

Double

例如,当调用需要包装类的方法并将其原始变量作为参数传递时,或者反之,会自动发生装箱和拆箱。这与 Java 类似,但 Groovy 更进一步。

在大多数情况下,你可以将原始类型视为完整的对象包装器等效物。例如,你可以在原始类型上调用 `.toString()` 或 `.equals(other)`。Groovy 会根据需要自动在引用和原始类型之间进行包装和拆箱。

以下是一个使用 `int` 的示例,它被声明为类中的静态字段(稍后讨论)。

class Foo {
    static int i
}

assert Foo.class.getDeclaredField('i').type == int.class           (1)
assert Foo.i.class != int.class && Foo.i.class == Integer.class    (2)
1 原始类型在字节码中得到尊重
2 在运行时查看字段显示它已被自动包装

现在你可能会担心,这意味着每次对原始类型的引用使用数学运算符时,都会产生拆箱和重新装箱原始类型的开销。但事实并非如此,因为 Groovy 会将你的运算符编译成它们的方法等效物并使用它们。此外,当调用接受原始类型参数的 Java 方法时,Groovy 会自动拆箱为原始类型,并自动装箱来自 Java 的原始类型方法返回值。但是,请注意 Java 的方法解析存在一些差异

1.2. 引用类型

除了原始类型,其他所有都是对象,并且具有定义其类型的关联类。我们将在稍后讨论类以及与类相关或类似类的东西,如接口、特征和记录。

我们可能会声明两个变量,类型为 String 和 List,如下所示:

String movie = 'The Matrix'
List actors = ['Keanu Reeves', 'Hugo Weaving']

1.3. 泛型

Groovy 在泛型方面沿用了 Java 的概念。在定义类和方法时,可以使用类型参数来创建泛型类、接口、方法或构造函数。

泛型类和方法的使用,无论它们是在 Java 还是 Groovy 中定义,都可能涉及提供类型参数。

我们可能会声明一个类型为“字符串列表”的变量,如下所示:

List<String> roles = ['Trinity', 'Morpheus']

Java 为了向后兼容 Java 的早期版本而采用类型擦除。动态 Groovy 可以被认为是更积极地应用类型擦除。通常,在编译时会检查较少的泛型类型信息。Groovy 的静态特性在泛型信息方面采用了与 Java 类似的检查。

2. 类

Groovy 类与 Java 类非常相似,并且在 JVM 级别上与 Java 类兼容。它们可以有方法、字段和属性(类似于 JavaBeans 属性,但样板代码更少)。类和类成员可以拥有与 Java 相同的修饰符(public、protected、private、static 等),但在源级别有一些细微差别,这些差别稍后会解释。

Groovy 类与 Java 类的主要区别在于:

  • 没有可见性修饰符的类或方法会自动成为 public(可以使用特殊注解来实现包私有可见性)。

  • 没有可见性修饰符的字段会自动转换为属性,这会减少代码的冗余,因为不需要显式的 getter 和 setter 方法。这方面将在字段和属性部分详细介绍。

  • 类不需要与它们的源文件定义具有相同的基本名称,但在大多数情况下强烈推荐这样做(另请参阅关于脚本的下一点)。

  • 一个源文件可以包含一个或多个类(但如果文件包含任何不在类中的代码,则它被视为脚本)。脚本只是具有一些特殊约定的类,并且将与它们的源文件具有相同的名称(因此不要在脚本中包含与脚本源文件同名的类定义)。

以下代码提供了一个示例类。

class Person {                       (1)

    String name                      (2)
    Integer age

    def increaseAge(Integer years) { (3)
        this.age += years
    }
}
1 类开始,名称为 `Person`
2 字符串字段和名为 `name` 的属性
3 方法定义

2.1. 普通类

普通类指的是顶级和具体的类。这意味着它们可以被实例化,不受任何其他类或脚本的限制。这样,它们只能是公共的(即使 `public` 关键字可能被省略)。类通过调用它们的构造函数,使用 `new` 关键字来实例化,如以下代码片段所示。

def p = new Person()

2.2. 内部类

内部类定义在另一个类内部。封闭类可以像往常一样使用内部类。另一方面,内部类可以访问其封闭类的成员,即使它们是私有的。除了封闭类,其他类不允许访问内部类。以下是一个示例:

class Outer {
    private String privateStr

    def callInnerMethod() {
        new Inner().methodA()       (1)
    }

    class Inner {                   (2)
        def methodA() {
            println "${privateStr}." (3)
        }
    }
}
1 内部类被实例化并调用其方法
2 内部类定义,在其封闭类内部
3 即使是私有的,内部类也能访问封闭类的字段

使用内部类有以下几个原因:

  • 它们通过将内部类隐藏起来,增加封装性,因为其他类不需要了解它。这也有助于实现更清晰的包和工作区。

  • 通过将仅被一个类使用的类进行分组,它们提供了良好的组织性。

  • 它们使代码更易于维护,因为内部类位于使用它们的类附近。

内部类通常是某个接口的实现,外部类需要该接口的方法。下面的代码演示了这种典型的使用模式,此处用于线程。

class Outer2 {
    private String privateStr = 'some string'

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

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

请注意,类 `Inner2` 的定义仅用于为类 `Outer2` 提供 `run` 方法的实现。在这种情况下,匿名内部类有助于消除冗余。该主题将在稍后介绍。

Groovy 3+ 也支持 Java 语法用于非静态内部类实例化,例如:

class Computer {
    class Cpu {
        int coreNumber

        Cpu(int coreNumber) {
            this.coreNumber = coreNumber
        }
    }
}

assert 4 == new Computer().new Cpu(4).coreNumber

2.2.1. 匿名内部类

上一个内部类 (`Inner2`) 的示例可以使用匿名内部类进行简化。以下代码可以实现相同的功能:

class Outer3 {
    private String privateStr = 'some string'

    def startThread() {
        new Thread(new Runnable() {      (1)
            void run() {
                println "${privateStr}."
            }
        }).start()                       (2)
    }
}
1 与上一节的最后一个示例相比,`new Inner2()` 被 `new Runnable()` 及其所有实现替换
2 方法 `start` 正常调用

因此,无需定义一个仅使用一次的新类。

2.2.2. 抽象类

抽象类代表通用概念,因此无法实例化,创建它们的目的是为了被子类继承。它们的成员包括字段/属性和抽象或具体方法。抽象方法没有实现,必须由具体子类实现。

abstract class Abstract {         (1)
    String name

    abstract def abstractMethod() (2)

    def concreteMethod() {
        println 'concrete'
    }
}
1 抽象类必须使用 `abstract` 关键字声明
2 抽象方法也必须使用 `abstract` 关键字声明

抽象类通常与接口进行比较。选择其中一个至少有两个重要区别。首先,抽象类可以包含字段/属性和具体方法,而接口只能包含抽象方法(方法签名)。此外,一个类可以实现多个接口,而它只能扩展一个类,无论是抽象类还是非抽象类。

2.3. 继承

Groovy 中的继承类似于 Java 中的继承。它提供了一种机制,允许子类(或派生类)重用父类(或超类)的代码或属性。通过继承关联的类形成继承层次结构。常见的行为和成员被推到层次结构的上层以减少重复。特化发生在子类中。

支持不同形式的继承:

  • 实现继承,其中子类重用来自超类或一个或多个特征的代码(方法、字段或属性)

  • 契约继承,其中一个类承诺提供超类中定义、或一个或多个特征接口中定义的特定抽象方法。

2.4. 超类

父类与子类共享可见的字段、属性或方法。子类最多可以有一个父类。`extends` 关键字紧跟在超类类型之前使用。

2.5. 接口

接口定义了类需要遵循的契约。接口只定义了需要实现的方法列表,但没有定义方法的实现。

interface Greeter {                                         (1)
    void greet(String name)                                 (2)
}
1 接口需要使用 `interface` 关键字声明
2 接口只定义方法签名

接口的方法总是 **public**。在接口中使用 `protected` 或 `private` 方法是错误的。

interface Greeter {
    protected void greet(String name)           (1)
}
1 使用 `protected` 会导致编译时错误

如果一个类在其 `implements` 列表中定义了接口,或者其任何超类定义了接口,则该类实现了该接口。

class SystemGreeter implements Greeter {                    (1)
    void greet(String name) {                               (2)
        println "Hello $name"
    }
}

def greeter = new SystemGreeter()
assert greeter instanceof Greeter                           (3)
1 `SystemGreeter` 使用 `implements` 关键字声明 `Greeter` 接口
2 然后实现所需的 `greet` 方法
3 `SystemGreeter` 的任何实例也是 `Greeter` 接口的实例

一个接口可以扩展另一个接口

interface ExtendedGreeter extends Greeter {                 (1)
    void sayBye(String name)
}
1 `ExtendedGreeter` 接口使用 `extends` 关键字扩展 `Greeter` 接口

值得注意的是,一个类要成为一个接口的实例,它必须是显式的。例如,以下类定义了 `greet` 方法,就像它在 `Greeter` 接口中声明的那样,但没有在其接口中声明 `Greeter`

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

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

换句话说,Groovy 不支持结构化类型。但是,可以使用 `as` 强制转换运算符在运行时使对象实例实现接口。

greeter = new DefaultGreeter()                              (1)
coerced = greeter as Greeter                                (2)
assert coerced instanceof Greeter                           (3)
1 创建一个不实现接口的 `DefaultGreeter` 实例
2 在运行时将实例强制转换为 `Greeter`
3 强制转换后的实例实现了 `Greeter` 接口

你可以看到有两个不同的对象:一个是源对象,一个 `DefaultGreeter` 实例,它不实现接口。另一个是 `Greeter` 的实例,它委托给被强制转换的对象。

Groovy 接口不支持像 Java 8 接口那样的默认实现。如果你正在寻找类似(但不完全相同)的东西,特征(traits)与接口相似,但允许默认实现以及本手册中描述的其他重要特性。

3. 类成员

3.1. 构造函数

构造函数是用于以特定状态初始化对象的特殊方法。与普通方法一样,一个类可以声明多个构造函数,只要每个构造函数具有唯一的类型签名。如果一个对象在构造过程中不需要任何参数,它可以使用无参构造函数。如果没有提供构造函数,Groovy 编译器将提供一个空的无参构造函数。

Groovy 支持两种调用风格:

  • 位置参数的使用方式类似于 Java 构造函数

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

3.1.1. 位置参数

要使用位置参数创建对象,相应的类需要声明一个或多个构造函数。在多个构造函数的情况下,每个构造函数都必须具有唯一的类型签名。构造函数也可以使用 groovy.transform.TupleConstructor 注解添加到类中。

通常,一旦声明了至少一个构造函数,就只能通过调用其构造函数之一来实例化该类。值得注意的是,在这种情况下,通常不能使用命名参数创建该类。Groovy 支持命名参数,只要该类包含一个无参构造函数,或者提供一个将 `Map` 参数作为第一个(并且可能唯一)参数的构造函数 - 详情请参阅下一节。

使用已声明构造函数有三种形式。第一种是正常的 Java 方式,使用 `new` 关键字。其他两种依赖于将列表强制转换为所需类型。在这种情况下,可以使用 `as` 关键字和静态类型化变量进行强制转换。

class PersonConstructor {
    String name
    Integer age

    PersonConstructor(name, age) {          (1)
        this.name = name
        this.age = age
    }
}

def person1 = new PersonConstructor('Marie', 1)  (2)
def person2 = ['Marie', 2] as PersonConstructor  (3)
PersonConstructor person3 = ['Marie', 3]         (4)
1 构造函数声明
2 构造函数调用,经典的 Java 方式
3 构造函数使用,使用 `as` 关键字强制转换
4 构造函数使用,在赋值中强制转换

3.1.2. 命名参数

如果没有声明(或声明了无参数)构造函数,则可以通过以映射(属性/值对)的形式传递参数来创建对象。这在希望允许多种参数组合的情况下非常方便。否则,通过使用传统的按位置参数,将需要声明所有可能的构造函数。支持将 `Map` 参数作为第一个(并且可能是唯一)参数的构造函数 - 这种构造函数也可以使用 groovy.transform.MapConstructor 注解添加。

class PersonWOConstructor {                                  (1)
    String name
    Integer age
}

def person4 = new PersonWOConstructor()                      (2)
def person5 = new PersonWOConstructor(name: 'Marie')         (3)
def person6 = new PersonWOConstructor(age: 1)                (4)
def person7 = new PersonWOConstructor(name: 'Marie', age: 2) (5)
1 未声明构造函数
2 实例化时未提供参数
3 实例化时提供了 `name` 参数
4 实例化时提供了 `age` 参数
5 实例化时提供了 `name` 和 `age` 参数

然而,需要强调的是,这种方法赋予了构造函数调用者更大的权力,同时也增加了调用者正确获取名称和值类型的责任。因此,如果需要更大的控制权,可能更倾向于使用位置参数声明构造函数。

备注:

  • 虽然上面的示例没有提供构造函数,但您也可以提供一个无参数构造函数或一个 `Map` 作为第一个参数的构造函数,通常它是唯一的参数。

  • 当没有声明(或声明了无参数)构造函数时,Groovy 会将命名构造函数调用替换为对无参数构造函数的调用,然后对每个提供的命名属性调用 setter。

  • 当第一个参数是 Map 时,Groovy 会将所有命名参数组合成一个 Map(无论顺序如何),并将该 Map 作为第一个参数提供。如果您的属性声明为 `final`(因为它们将在构造函数中设置,而不是事后通过 setter 设置),这可能是一个好方法。

  • 您可以通过同时提供位置构造函数以及无参数或 Map 构造函数来支持命名和位置构造。

  • 您可以通过拥有一个第一个参数是 Map 但也有额外位置参数的构造函数来支持混合构造。请谨慎使用此样式。

3.2. 方法

Groovy 方法与其他语言的方法非常相似。一些特殊之处将在接下来的子节中展示。

3.2.1. 方法定义

方法用返回类型或 `def` 关键字定义,以使返回类型未定型。方法还可以接收任意数量的参数,这些参数可能没有明确声明其类型。Java 修饰符可以正常使用,如果没有提供可见性修饰符,则方法是 public 的。

Groovy 中的方法总是返回某个值。如果未提供 `return` 语句,则返回执行的最后一行中评估的值。例如,请注意以下方法都没有使用 `return` 关键字。

def someMethod() { 'method called' }                           (1)
String anotherMethod() { 'another method called' }             (2)
def thirdMethod(param1) { "$param1 passed" }                   (3)
static String fourthMethod(String param1) { "$param1 passed" } (4)
1 没有声明返回类型且没有参数的方法
2 具有显式返回类型且没有参数的方法
3 带有一个未定义类型的参数的方法
4 带 String 参数的静态方法

3.2.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)  (1)
foo(23, name: 'Marie', age: 1)  (2)
1 方法调用,带有额外的 `number` 参数,类型为 `Integer`
2 方法调用,参数顺序已更改

如果我们没有将 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)  (1)
1 方法调用抛出 `groovy.lang.MissingMethodException: No signature of method: foo() is applicable for argument types: (LinkedHashMap, Integer) values: [[name:Marie, age:1], 23]`,因为命名参数 `Map` 没有定义为第一个参数

如果我们将命名参数替换为显式 `Map` 参数,则可以避免上述异常

def foo(Integer number, Map args) { "${args.name}: ${args.age}, and the number is ${number}" }
foo(23, [name: 'Marie', age: 1])  (1)
1 显式 `Map` 参数代替命名参数使调用有效
尽管 Groovy 允许你混合命名参数和位置参数,但这可能会导致不必要的混淆。请谨慎混合命名参数和位置参数。

3.2.3. 默认参数

默认参数使参数可选。如果未提供参数,则方法假定一个默认值。

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

参数从右侧丢弃,但强制参数永远不会被丢弃。

def baz(a = 'a', int b, c = 'c', boolean d, e = 'e') { "$a $b $c $d $e" }

assert baz(42, true) == 'a 42 c true e'
assert baz('A', 42, true) == 'A 42 c true e'
assert baz('A', 42, 'C', true) == 'A 42 C true e'
assert baz('A', 42, 'C', true, 'E') == 'A 42 C true E'

同样的规则适用于构造函数和方法。如果使用 `@TupleConstructor`,则适用额外的配置选项。

3.2.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` 作为可变参数调用带有可变参数的方法,则该参数将是 `null`,而不是长度为 1 且仅包含 `null` 的数组。

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

如果使用数组作为参数调用可变参数方法,则该参数将是该数组,而不是长度为 1 且仅包含给定数组的数组。

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

另一个重要的点是可变参数与方法重载的结合。在方法重载的情况下,Groovy 将选择最具体的方法。例如,如果一个方法 `foo` 接受类型为 `T` 的可变参数,而另一个方法 `foo` 也接受一个类型为 `T` 的参数,则首选第二个方法。

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

3.2.5. 方法选择算法

动态 Groovy 支持多重分派(又称多方法)。当调用方法时,实际调用的方法是根据方法参数的运行时类型动态确定的。首先会考虑方法名和参数数量(包括对可变参数的允许),然后是每个参数的类型。考虑以下方法定义:

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

或许正如预期,使用 `String` 和 `Integer` 参数调用 `method` 会调用我们的第三个方法定义。

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

这里更有趣的是当类型在编译时未知时。也许参数被声明为 `Object` 类型(在我们的例子中是一个此类对象的列表)。Java 会确定在所有情况下都将选择 `method(Object, Object)` 变体(除非使用强制转换),但正如以下示例所示,Groovy 使用运行时类型,并将分别调用我们每个方法一次(通常不需要强制转换)。

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

对于我们三次方法调用中的前两次,找到了参数类型的精确匹配。对于第三次调用,没有找到 `method(Integer, Integer)` 的精确匹配,但 `method(Object, Object)` 仍然有效并将被选中。

方法选择就是从具有兼容参数类型的有效方法候选者中找到最接近的匹配。因此,`method(Object, Object)` 对于前两次调用也有效,但不如类型精确匹配的变体匹配得近。为了确定最接近的匹配,运行时有一个概念,即实际参数类型与声明的参数类型的距离,并尝试最小化所有参数的总距离。

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

方面 示例

直接实现的接口比继承层次结构中更远的接口更匹配。

给定这些接口和方法定义:

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

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

直接实现的接口将匹配

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'

3.2.6. 异常声明

Groovy 自动允许您像处理未检查异常一样处理已检查异常。这意味着您不需要声明方法可能抛出的任何已检查异常,如下例所示,如果找不到文件,它可能会抛出 `FileNotFoundException`

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

shouldFail(FileNotFoundException) {
    badRead()
}

您也无需将上一个示例中对 `badRead` 方法的调用包含在 try/catch 块中 - 尽管您可以根据需要这样做。

如果您希望声明代码可能抛出的任何异常(无论是否已检查),您都可以这样做。添加异常不会改变其他 Groovy 代码使用代码的方式,但可以视为代码人类读者的文档。这些异常将成为字节码中方法声明的一部分,因此如果您的代码可能从 Java 调用,则包含它们可能会很有用。以下示例说明了使用显式已检查异常声明:

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

shouldFail(FileNotFoundException) {
    badRead()
}

3.3. 字段和属性

3.3.1. 字段

字段是类、接口或特征的成员,用于存储数据。在 Groovy 源文件中定义的字段具有:

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

  • 一个或多个可选的修饰符(`static`、`final`、`synchronized`)

  • 一个可选的类型

  • 一个强制性的名称

class Data {
    private int id                                  (1)
    protected String description                    (2)
    public static final boolean DEBUG = false       (3)
}
1 一个名为 `id` 的 `private` 字段,类型为 `int`
2 一个名为 `description` 的 `protected` 字段,类型为 `String`
3 一个名为 `DEBUG` 的 `public static final` 字段,类型为 `boolean`

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

class Data {
    private String id = IDGenerator.next() (1)
    // ...
}
1 私有字段 `id` 使用 `IDGenerator.next()` 初始化

可以省略字段的类型声明。然而,这被认为是一种不好的做法,通常情况下,为字段使用强类型是一个好主意。

class BadPractice {
    private mapping                         (1)
}
class GoodPractice {
    private Map<String,String> mapping      (2)
}
1 字段 `mapping` 没有声明类型
2 字段 `mapping` 具有强类型

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

3.3.2. 属性

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

  • 没有访问修饰符(没有 `public`、`protected` 或 `private`)

  • 一个或多个可选的修饰符(`static`、`final`、`synchronized`)

  • 一个可选的类型

  • 一个强制性的名称

Groovy 将相应地生成 getter/setter。例如:

class Person {
    String name                             (1)
    int age                                 (2)
}
1 创建一个支持 `private String name` 字段、`getName` 方法和 `setName` 方法
2 创建一个支持 `private int age` 字段、`getAge` 方法和 `setAge` 方法

如果属性被声明为 `final`,则不会生成 setter

class Person {
    final String name                   (1)
    final int age                       (2)
    Person(String name, int age) {
        this.name = name                (3)
        this.age = age                  (4)
    }
}
1 定义一个类型为 `String` 的只读属性
2 定义一个类型为 `int` 的只读属性
3 将 `name` 参数赋值给 `name` 字段
4 将 `age` 参数赋值给 `age` 字段

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

class Person {
    String name
    void name(String name) {
        this.name = "Wonder $name"      (1)
    }
    String title() {
        this.name                       (2)
    }
}
def p = new Person()
p.name = 'Diana'                        (3)
assert p.name == 'Diana'                (4)
p.name('Woman')                         (5)
assert p.title() == 'Wonder Woman'      (6)
1 `this.name` 将直接访问字段,因为属性是从定义它的类内部访问的
2 类似地,读取访问直接在 `name` 字段上完成
3 对属性的写入访问在 `Person` 类外部完成,因此它将隐式调用 `setName`
4 对属性的读取访问在 `Person` 类外部完成,因此它将隐式调用 `getName`
5 这将调用 `Person` 上的 `name` 方法,该方法直接访问字段
6 这将调用 `Person` 上的 `title` 方法,该方法直接读取字段

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

借助实例的元 `properties` 字段,可以列出类的属性。

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

按照惯例,Groovy 会识别属性,即使没有提供支持字段,只要有遵循 Java Beans 规范的 getter 或 setter。例如:

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'                      (1)
assert p.age == 42                  (2)
p.groovy = true                     (3)
1 允许写入 `p.name`,因为存在伪属性 `name`
2 允许读取 `p.age`,因为存在伪只读属性 `age`
3 允许写入 `p.groovy`,因为存在伪只写属性 `groovy`

这种语法糖是 Groovy 中许多 DSL 的核心。

属性命名约定

通常建议属性名称的前两个字母小写,对于多词属性,使用驼峰命名法。在这些情况下,生成的 getter 和 setter 的名称将由属性名称大写并加上 `get` 或 `set` 前缀(或者对于布尔型 getter,可选地加上“is”)组成。因此,`getLength` 将是 `length` 属性的 getter,`setFirstName` 将是 `firstName` 属性的 setter。`isEmpty` 可能是名为 `empty` 的属性的 getter 方法名称。

以大写字母开头的属性名称将只有前缀。因此,`Foo` 属性是允许的,即使它不遵循推荐的命名约定。对于此属性,访问器方法将是 `setFoo` 和 `getFoo`。其结果是,您不能同时拥有 `foo` 和 `Foo` 属性,因为它们将具有相同的命名访问器方法。

JavaBeans 规范对通常可能是首字母缩写词的属性做出了特殊规定。如果属性名称的前两个字母是大写,则不执行大写(或者更重要的是,如果从访问器方法名称生成属性名称,则不执行小写)。因此,`getURL` 将是 `URL` 属性的 getter。

由于 JavaBeans 规范中特殊的“缩写处理”属性命名逻辑,属性名称的转换是不对称的。这导致了一些奇怪的边缘情况。Groovy 采用了一种命名约定,避免了一个可能看起来有点奇怪但在 Groovy 设计时流行并(到目前为止)出于历史原因保留下来的歧义。Groovy 查看属性名称的第二个字母。如果它是大写字母,则该属性被视为缩写样式属性,不执行大写,否则执行正常大写。尽管我们从不推荐这样做,但它确实允许您拥有看起来像“重复命名”的属性,例如,您可以拥有 `aProp` 和 `AProp`,或者 `pNAME` 和 `PNAME`。getter 分别是 `getaProp` 和 `getAProp`,以及 `getpNAME` 和 `getPNAME`。

属性上的修饰符

我们已经看到通过省略可见性修饰符来定义属性。通常,任何其他修饰符,例如 `transient`,都会复制到字段。值得注意的两个特殊情况是:

  • 我们之前看到的 `final` 用于只读属性,它会复制到支持字段,但也会导致不定义 setter

  • `static` 复制到支持字段,但也会导致访问器方法变为静态

如果您希望将 `final` 等修饰符也传递给访问器方法,您可以手动编写属性,或者考虑使用分离属性定义

属性上的注解

注解,包括那些与 AST 转换相关的注解,都会复制到属性的支持字段中。这允许将适用于字段的 AST 转换应用于属性,例如:

class Animal {
    int lowerCount = 0
    @Lazy String name = { lower().toUpperCase() }()
    String lower() { lowerCount++; 'sloth' }
}

def a = new Animal()
assert a.lowerCount == 0  (1)
assert a.name == 'SLOTH'  (2)
assert a.lowerCount == 1  (3)
1 确认没有急切初始化
2 正常属性访问
3 确认在属性访问时初始化
使用显式支持字段分割属性定义

当您的类设计遵循与常见 JavaBean 实践一致的特定约定时,Groovy 的属性语法是一种方便的速记。如果您的类不完全符合这些约定,您当然可以像在 Java 中一样,手动编写 getter、setter 和支持字段。然而,Groovy 确实提供了分离定义功能,它仍然提供了简化的语法,同时允许对约定进行微调。对于分离定义,您编写一个具有相同名称和类型的字段和一个属性。字段或属性中只能有一个具有初始值。

对于分离属性,字段上的注解保留在属性的支持字段上。定义中属性部分上的注解将复制到 getter 和 setter 方法上。

这种机制允许属性用户在标准属性定义不完全符合其需求时,使用许多常见的变体。例如,如果支持字段应为 `protected` 而不是 `private`:

class HasPropertyWithProtectedField {
    protected String name  (1)
    String name            (2)
}
1 `name` 属性的保护性支持字段,而不是正常的私有字段
2 声明 `name` 属性

或者,同样的例子,但使用包私有支持字段

class HasPropertyWithPackagePrivateField {
    String name                (1)
    @PackageScope String name  (2)
}
1 声明 `name` 属性
2 `name` 属性的包私有支持字段,而不是正常的私有字段

最后一个例子,我们可能希望将与方法相关的 AST 转换或一般注解应用于 setter/getter,例如,使访问器同步:

class HasPropertyWithSynchronizedAccessorMethods {
    private String name        (1)
    @Synchronized String name  (2)
}
1 `name` 属性的支持字段
2 声明带有 setter/getter 注解的 `name` 属性
显式访问器方法

如果类中存在 getter 或 setter 的显式定义,则不会自动生成访问器方法。这允许您在需要时修改此类 getter 或 setter 的正常行为。通常不考虑继承的访问器方法,但如果继承的访问器方法被标记为 final,则为了遵守方法不能被子类化的 `final` 要求,也不会生成额外的访问器方法。

4. 注解

4.1. 注解定义

注解是一种特殊的接口,专门用于对代码元素进行注解。注解是一种类型,其超接口是 java.lang.annotation.Annotation 接口。注解的声明方式与接口非常相似,使用 `@interface` 关键字。

@interface SomeAnnotation {}

注解可以定义成员,形式为无体方法和可选的默认值。可能的成员类型仅限于:

例如

@interface SomeAnnotation {
    String value()                          (1)
}
@interface SomeAnnotation {
    String value() default 'something'      (2)
}
@interface SomeAnnotation {
    int step()                              (3)
}
@interface SomeAnnotation {
    Class appliesTo()                       (4)
}
@interface SomeAnnotation {}
@interface SomeAnnotations {
    SomeAnnotation[] value()                (5)
}
enum DayOfWeek { mon, tue, wed, thu, fri, sat, sun }
@interface Scheduled {
    DayOfWeek dayOfWeek()                   (6)
}
1 定义一个类型为 `String` 的 `value` 成员的注解
2 定义一个类型为 `String` 的 `value` 成员,默认值为 `something` 的注解
3 定义一个类型为原始类型 `int` 的 `step` 成员的注解
4 定义一个类型为 `Class` 的 `appliesTo` 成员的注解
5 定义一个 `value` 成员,其类型是另一个注解类型的数组的注解
6 定义一个 `dayOfWeek` 成员,其类型为枚举类型 `DayOfWeek` 的注解

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

4.1.1. 注解放置

注解可以应用于代码的各种元素

@SomeAnnotation                 (1)
void someMethod() {
    // ...
}

@SomeAnnotation                 (2)
class SomeClass {}

@SomeAnnotation String var      (3)
1 `@SomeAnnotation` 应用于 `someMethod` 方法
2 `@SomeAnnotation` 应用于 `SomeClass` 类
3 `@SomeAnnotation` 应用于 `var` 变量

为了限制注解的应用范围,需要在注解定义上使用 java.lang.annotation.Target 注解来声明。例如,以下是如何声明一个注解可以应用于类或方法:

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

@Target([ElementType.METHOD, ElementType.TYPE])     (1)
@interface SomeAnnotation {}                        (2)
1 `@Target` 注解用于为注解添加范围注解。
2 因此,`@SomeAnnotation` 将只允许用于 `TYPE` 或 `METHOD`

可能的目标列表可在 java.lang.annotation.ElementType 中找到。

4.1.2. 注解成员值

使用注解时,需要设置至少所有没有默认值的成员。例如:

@interface Page {
    int statusCode()
}

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

但是,如果 `value` 成员是唯一被设置的,则可以在注解的值声明中省略 `value=`

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

@Page(value='/home')                    (1)
void home() {
    // ...
}

@Page('/users')                         (2)
void userList() {
    // ...
}

@Page(value='error',statusCode=404)     (3)
void notFound() {
    // ...
}
1 我们可以省略 `statusCode`,因为它有默认值,但 `value` 需要设置
2 由于 `value` 是唯一没有默认值的强制成员,我们可以省略 `value=`
3 如果 `value` 和 `statusCode` 都需要设置,则需要为默认 `value` 成员使用 `value=`

4.1.3. 保留策略

注解的可见性取决于其保留策略。注解的保留策略使用 java.lang.annotation.Retention 注解设置。

import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy

@Retention(RetentionPolicy.SOURCE)                   (1)
@interface SomeAnnotation {}                         (2)
1 `@Retention` 注解为 `@SomeAnnotation` 注解
2 因此 `@SomeAnnotation` 将具有 `SOURCE` 保留策略

可能的保留目标列表和描述可在 java.lang.annotation.RetentionPolicy 枚举中找到。选择通常取决于您是希望注解在编译时可见还是在运行时可见。

4.1.4. 闭包注解参数

Groovy 中注解的一个有趣特性是你可以使用闭包作为注解值。因此,注解可以与各种表达式一起使用,并且仍然支持 IDE。例如,想象一个框架,你希望根据 JDK 版本或操作系统等环境约束来执行某些方法。你可以编写以下代码:

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'
    }
}

为了让 `@OnlyIf` 注解接受 `Closure` 作为参数,你只需要将 `value` 声明为 `Class`。

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

为了完成这个例子,我们来编写一个示例运行器,它将使用这些信息。

class Runner {
    static <T> T run(Class<T> taskClass) {
        def tasks = taskClass.newInstance()                                         (1)
        def params = [jdk: 6, windows: false]                                       (2)
        tasks.class.declaredMethods.each { m ->                                     (3)
            if (Modifier.isPublic(m.modifiers) && m.parameterTypes.length == 0) {   (4)
                def onlyIf = m.getAnnotation(OnlyIf)                                (5)
                if (onlyIf) {
                    Closure cl = onlyIf.value().newInstance(tasks,tasks)            (6)
                    cl.delegate = params                                            (7)
                    if (cl()) {                                                     (8)
                        m.invoke(tasks)                                             (9)
                    }
                } else {
                    m.invoke(tasks)                                                 (10)
                }
            }
        }
        tasks                                                                       (11)
    }
}
1 创建作为参数传递的类(任务类)的新实例
2 模拟一个 JDK 6 且非 Windows 的环境
3 遍历任务类的所有声明方法
4 如果方法是公共的且不带参数
5 尝试查找 `@OnlyIf` 注解
6 如果找到,获取 `value` 并从中创建一个新的 `Closure`
7 将闭包的 `delegate` 设置为我们的环境变量
8 调用闭包,即注解闭包。它将返回一个 `boolean`
9 如果为 `true`,则调用方法
10 如果方法没有被 `@OnlyIf` 注解,仍然执行该方法
11 之后,返回任务对象

然后运行器可以这样使用:

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

4.2. 元注解

4.2.1. 声明元注解

元注解,也称为注解别名,是在编译时被其他注解替换的注解(一个元注解是一个或多个注解的别名)。元注解可以用于减少涉及多个注解的代码大小。

我们从一个简单的例子开始。假设您有 `@Service` 和 `@Transactional` 注解,并且您想用这两个注解来注解一个类:

@Service
@Transactional
class MyTransactionalService {}

鉴于可以添加到同一个类的注解数量之多,元注解可以通过用一个具有相同语义的单个注解来减少这两个注解的使用。例如,我们可能希望这样写:

@TransactionalService                           (1)
class MyTransactionalService {}
1 `@TransactionalService` 是一个元注解

元注解被声明为常规注解,但用 `@AnnotationCollector` 和它所收集的注解列表进行注解。在我们的例子中,`@TransactionalService` 注解可以这样写:

import groovy.transform.AnnotationCollector

@Service                                        (1)
@Transactional                                  (2)
@AnnotationCollector                            (3)
@interface TransactionalService {
}
1 用 `@Service` 注解元注解
2 用 `@Transactional` 注解元注解
3 用 `@AnnotationCollector` 注解元注解

4.2.2. 元注解的行为

Groovy 支持预编译源形式元注解。这意味着您的元注解可以预编译,也可以将其放在您当前正在编译的相同源树中。

信息:元注解是 Groovy 独有的特性。您无法使用元注解来注解 Java 类,并期望它与 Groovy 中表现相同。同样,您不能在 Java 中编写元注解:元注解定义**和**使用都必须是 Groovy 代码。但是,您可以在元注解中愉快地收集 Java 注解和 Groovy 注解。

当 Groovy 编译器遇到用元注解注解的类时,它会**替换**它所收集的注解。因此,在我们之前的示例中,它会将 `@TransactionalService` 替换为 `@Transactional` 和 `@Service`。

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

从元注解到收集注解的转换是在语义分析编译阶段执行的。

除了用收集的注解替换别名之外,元注解还能够处理它们,包括参数。

4.2.3. 元注解参数

元注解可以收集带参数的注解。为了说明这一点,我们将设想两个注解,每个注解接受一个参数:

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

假设您想创建一个名为 `@Explosive` 的元注解

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

默认情况下,当注解被替换时,它们将获得注解参数值**,如同它们在别名中定义的那样**。更有趣的是,元注解支持覆盖特定值:

@Explosive(after=0)                 (1)
class Bomb {}
1 提供给 `@Explosive` 作为参数的 `after` 值将覆盖 `Timeout` 注解中定义的值

如果两个注解定义了相同的参数名,默认处理器会将注解值复制到所有接受该参数的注解中。

@Retention(RetentionPolicy.RUNTIME)
public @interface Foo {
   String value()                                   (1)
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Bar {
    String value()                                  (2)
}

@Foo
@Bar
@AnnotationCollector
public @interface FooBar {}                         (3)

@Foo('a')
@Bar('b')
class Bob {}                                        (4)

assert Bob.getAnnotation(Foo).value() == 'a'        (5)
println Bob.getAnnotation(Bar).value() == 'b'       (6)

@FooBar('a')
class Joe {}                                        (7)
assert Joe.getAnnotation(Foo).value() == 'a'        (8)
println Joe.getAnnotation(Bar).value() == 'a'       (9)
1 `@Foo` 注解定义了类型为 `String` 的 `value` 成员
2 `@Bar` 注解也定义了类型为 `String` 的 `value` 成员
3 `@FooBar` 元注解聚合了 `@Foo` 和 `@Bar`
4 类 `Bob` 被 `@Foo` 和 `@Bar` 注解
5 `Bob` 上的 `@Foo` 注解的值是 `a`
6 而 `Bob` 上的 `@Bar` 注解的值是 `b`
7 类 `Joe` 被 `@FooBar` 注解
8 那么 `Joe` 上的 `@Foo` 注解的值是 `a`
9 `Joe` 上的 `@Bar` 注解的值也是 `a`

在第二种情况下,元注解值被复制到 `@Foo` 和 `@Bar` 两个注解中。

如果收集的注解使用不兼容的类型定义相同的成员,则会发生编译时错误。例如,如果上例中 `@Foo` 定义了一个 `String` 类型的 `value`,但 `@Bar` 定义了一个 `int` 类型的 `value`。

但是,可以自定义元注解的行为,并描述收集的注解如何展开。我们很快会探讨如何做到这一点,但首先需要介绍一个高级处理选项。

4.2.4. 处理元注解中的重复注解

`@AnnotationCollector` 注解支持 `mode` 参数,该参数可用于改变默认处理器在存在重复注解时处理注解替换的方式。

信息:自定义处理器(接下来讨论)可能支持或不支持此参数。

例如,假设您创建了一个包含 `@ToString` 注解的元注解,然后将您的元注解放置在一个已经具有显式 `@ToString` 注解的类上。这应该是一个错误吗?两个注解都应该应用吗?一个是否优先于另一个?没有正确的答案。在某些情况下,这些答案中的任何一个都可能非常合适。因此,Groovy 没有尝试预先确定处理重复注解问题的正确方法,而是允许您编写自己的自定义元注解处理器(接下来介绍),并允许您在 AST 转换中编写任何您喜欢的检查逻辑——AST 转换是聚合的常见目标。话虽如此,通过简单地设置 `mode`,许多通常预期的场景都可以在任何额外编码中自动为您处理。`mode` 参数的行为由所选的 `AnnotationCollectorMode` 枚举值确定,并总结在下表中。

模式

描述

DUPLICATE

注解集合中的注解将始终被插入。所有转换运行后,如果存在多个注解(不包括那些带有 SOURCE 保留策略的注解),将是一个错误。

PREFER_COLLECTOR

来自收集器的注解将被添加,任何具有相同名称的现有注解都将被移除。

PREFER_COLLECTOR_MERGED

来自收集器的注解将被添加,任何具有相同名称的现有注解都将被移除,但现有注解中发现的任何新参数都将合并到添加的注解中。

PREFER_EXPLICIT

如果找到任何具有相同名称的现有注解,则来自收集器的注解将被忽略。

PREFER_EXPLICIT_MERGED

如果找到任何具有相同名称的现有注解,则来自收集器的注解将被忽略,但收集器注解上的任何新参数都将添加到现有注解中。

4.2.5. 自定义元注解处理器

自定义注解处理器将允许您选择如何将元注解扩展为收集的注解。在这种情况下,元注解的行为完全由您决定。为此,您必须:

为了说明这一点,我们将探讨元注解 `@CompileDynamic` 是如何实现的。

`@CompileDynamic` 是一个元注解,它自身扩展为 `@CompileStatic(TypeCheckingMode.SKIP)`。问题是默认的元注解处理器不支持枚举,而注解值 `TypeCheckingMode.SKIP` 就是其中之一。

这里天真的实现是行不通的

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

相反,我们将这样定义它:

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

您可能首先注意到,我们的接口不再用 `@CompileStatic` 注解。这是因为我们转而依赖于 `processor` 参数,它引用了一个将**生成**注解的类。

下面是自定义处理器的实现方式:

CompileDynamicProcessor.groovy
@CompileStatic                                                                  (1)
class CompileDynamicProcessor extends AnnotationCollectorTransform {            (2)
    private static final ClassNode CS_NODE = ClassHelper.make(CompileStatic)    (3)
    private static final ClassNode TC_NODE = ClassHelper.make(TypeCheckingMode) (4)

    List<AnnotationNode> visit(AnnotationNode collector,                        (5)
                               AnnotationNode aliasAnnotationUsage,             (6)
                               AnnotatedNode aliasAnnotated,                    (7)
                               SourceUnit source) {                             (8)
        def node = new AnnotationNode(CS_NODE)                                  (9)
        def enumRef = new PropertyExpression(
            new ClassExpression(TC_NODE), "SKIP")                               (10)
        node.addMember("value", enumRef)                                        (11)
        Collections.singletonList(node)                                         (12)
    }
}
1 我们的自定义处理器是用 Groovy 编写的,为了更好的编译性能,我们使用静态编译
2 自定义处理器必须扩展 org.codehaus.groovy.transform.AnnotationCollectorTransform
3 创建一个表示 `@CompileStatic` 注解类型的类节点
4 创建一个表示 `TypeCheckingMode` 枚举类型的类节点
5 `collector` 是在元注解中找到的 `@AnnotationCollector` 节点。通常不使用。
6 `aliasAnnotationUsage` 是正在展开的元注解,这里是 `@CompileDynamic`
7 `aliasAnnotated` 是用元注解注解的节点
8 `sourceUnit` 是正在编译的 `SourceUnit`
9 我们为 `@CompileStatic` 创建一个新的注解节点
10 我们创建一个等同于 `TypeCheckingMode.SKIP` 的表达式
11 我们将该表达式添加到注解节点,现在是 `@CompileStatic(TypeCheckingMode.SKIP)`
12 返回生成的注解

在这个例子中,`visit` 方法是唯一需要被重写的方法。它旨在返回一个注解节点列表,这些节点将被添加到用元注解注解的节点中。在这个例子中,我们返回一个对应于 `@CompileStatic(TypeCheckingMode.SKIP)` 的单个注解节点。

5. 特征(Traits)

特征是语言的一种结构化构造,允许:

  • 行为组合

  • 接口的运行时实现

  • 行为覆盖

  • 与静态类型检查/编译兼容

它们可以被视为既包含**默认实现**又包含**状态**的**接口**。特征使用 `trait` 关键字定义:

trait FlyingAbility {                           (1)
        String fly() { "I'm flying!" }          (2)
}
1 特征的声明
2 特征内部方法的声明

然后它可以像普通接口一样使用 `implements` 关键字。

class Bird implements FlyingAbility {}          (1)
def b = new Bird()                              (2)
assert b.fly() == "I'm flying!"                 (3)
1 将特征 `FlyingAbility` 添加到 `Bird` 类的能力中
2 实例化一个新的 `Bird`
3 `Bird` 类自动获得 `FlyingAbility` 特征的行为

特征支持广泛的功能,从简单的组合到测试,本节将详细描述。

5.1. 方法

5.1.1. 公有方法

在特征中声明方法可以像类中的任何常规方法一样完成。

trait FlyingAbility {                           (1)
        String fly() { "I'm flying!" }          (2)
}
1 特征的声明
2 特征内部方法的声明

5.1.2. 抽象方法

此外,特征也可以声明抽象方法,因此需要在实现该特征的类中实现这些方法。

trait Greetable {
    abstract String name()                              (1)
    String greeting() { "Hello, ${name()}!" }           (2)
}
1 实现类必须声明 `name` 方法
2 可以与具体方法混合使用

然后这个特征可以这样使用:

class Person implements Greetable {                     (1)
    String name() { 'Bob' }                             (2)
}

def p = new Person()
assert p.greeting() == 'Hello, Bob!'                    (3)
1 实现特征 `Greetable`
2 由于 `name` 是抽象的,因此需要实现它
3 然后可以调用 `greeting`

5.1.3. 私有方法

特征也可以定义私有方法。这些方法不会出现在特征契约接口中。

trait Greeter {
    private String greetingMessage() {                      (1)
        'Hello from a private method!'
    }
    String greet() {
        def m = greetingMessage()                           (2)
        println m
        m
    }
}
class GreetingMachine implements Greeter {}                 (3)
def g = new GreetingMachine()
assert g.greet() == "Hello from a private method!"          (4)
try {
    assert g.greetingMessage()                              (5)
} catch (MissingMethodException e) {
    println "greetingMessage is private in trait"
}
1 在特征中定义一个私有方法 `greetingMessage`
2 公共 `greet` 消息默认调用 `greetingMessage`
3 创建一个实现该特征的类
4 可以调用 `greet`
5 但不能调用 `greetingMessage`
特征仅支持 `public` 和 `private` 方法。不支持 `protected` 和 `package private` 作用域。

5.1.4. 最终方法

如果我们有一个类实现了一个特征,从概念上讲,来自特征方法的实现是“继承”到类中的。但是,实际上,没有包含此类实现的基类。相反,它们直接编织到类中。方法上的 final 修饰符仅指示编织方法的修饰符将是什么。虽然继承和覆盖或多重继承具有相同签名但混合 final 和非 final 变体的方法可能会被认为是糟糕的风格,但 Groovy 并不禁止这种情况。正常的方法选择适用,并且所使用的修饰符将由结果方法确定。如果您希望特征实现方法不能被覆盖,您可以考虑创建一个实现所需特征的基类。

5.2. `this` 的含义

`this` 代表实现实例。将特征视为超类。这意味着当你这样写时:

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

然后调用

foo.whoAmI()

将返回相同的实例

assert foo.whoAmI().is(foo)

5.3. 接口

特征可以实现接口,在这种情况下,接口使用 `implements` 关键字声明。

interface Named {                                       (1)
    String name()
}
trait Greetable implements Named {                      (2)
    String greeting() { "Hello, ${name()}!" }
}
class Person implements Greetable {                     (3)
    String name() { 'Bob' }                             (4)
}

def p = new Person()
assert p.greeting() == 'Hello, Bob!'                    (5)
assert p instanceof Named                               (6)
assert p instanceof Greetable                           (7)
1 一个普通接口的声明
2 将 `Named` 添加到已实现接口列表
3 声明一个实现 `Greetable` 特征的类
4 实现缺失的 `name` 方法
5 `greeting` 实现来自特征
6 确保 `Person` 实现 `Named` 接口
7 确保 `Person` 实现 `Greetable` 特征

5.4. 属性

特征可以定义属性,如以下示例所示:

trait Named {
    String name                             (1)
}
class Person implements Named {}            (2)
def p = new Person(name: 'Bob')             (3)
assert p.name == 'Bob'                      (4)
assert p.getName() == 'Bob'                 (5)
1 在特征内部声明一个 `name` 属性
2 声明一个实现该特征的类
3 该属性自动可见
4 可以使用常规属性访问器访问它
5 或者使用常规 getter 语法

5.5. 字段

5.5.1. 私有字段

由于特性允许使用私有方法,因此使用私有字段来存储状态也可能很有趣。特性将允许您这样做:

trait Counter {
    private int count = 0                   (1)
    int count() { count += 1; count }       (2)
}
class Foo implements Counter {}             (3)
def f = new Foo()
assert f.count() == 1                       (4)
assert f.count() == 2
1 在特征中声明一个私有字段 `count`
2 声明一个公共方法 `count`,它增加计数器并返回它
3 声明一个实现 `Counter` 特征的类
4 `count` 方法可以使用私有字段来保持状态
这是与 Java 8 虚拟扩展方法 的主要区别。虽然虚拟扩展方法不携带状态,但特征可以。此外,Groovy 中的特征从 Java 6 开始就受支持,因为它们的实现不依赖于虚拟扩展方法。这意味着即使特征从 Java 类中可以看作是常规接口,该接口也**不会**有默认方法,只有抽象方法。

5.5.2. 公有字段

公共字段的工作方式与私有字段相同,但为了避免菱形问题,字段名称在实现类中会被重映射。

trait Named {
    public String name                      (1)
}
class Person implements Named {}            (2)
def p = new Person()                        (3)
p.Named__name = 'Bob'                       (4)
1 在特性中声明一个公共**字段**
2 声明一个实现该特性的类
3 创建该类的一个实例
4 公共字段可用,但已重命名

字段的名称取决于特征的完全限定名。包中的所有点(` .` )都替换为下划线(` _` ),最终名称包含一个双下划线。因此,如果字段的类型是 `String`,包的名称是 `my.package`,特征的名称是 `Foo`,字段的名称是 `bar`,在实现类中,公共字段将显示为

String my_package_Foo__bar
虽然特性支持公共字段,但不推荐使用它们,并被视为一种不好的实践。

5.6. 行为组合

特征可以用来以受控方式实现多重继承。例如,我们可以有以下特征:

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

以及一个同时实现这两个特征的类

class Duck implements FlyingAbility, SpeakingAbility {} (1)

def d = new Duck()                                      (2)
assert d.fly() == "I'm flying!"                         (3)
assert d.speak() == "I'm speaking!"                     (4)
1 `Duck` 类同时实现了 `FlyingAbility` 和 `SpeakingAbility`
2 创建一个新的 `Duck` 实例
3 我们可以调用 `FlyingAbility` 中的 `fly` 方法
4 也可以调用 `SpeakingAbility` 中的 `speak` 方法

特征鼓励对象之间能力的重用,以及通过组合现有行为来创建新类。

5.7. 覆盖默认方法

特征为方法提供默认实现,但可以在实现类中覆盖它们。例如,我们可以稍微修改上面的例子,让鸭子嘎嘎叫。

class Duck implements FlyingAbility, SpeakingAbility {
    String quack() { "Quack!" }                         (1)
    String speak() { quack() }                          (2)
}

def d = new Duck()
assert d.fly() == "I'm flying!"                         (3)
assert d.quack() == "Quack!"                            (4)
assert d.speak() == "Quack!"                            (5)
1 定义 `Duck` 特有的方法,名为 `quack`
2 覆盖 `speak` 的默认实现,以便我们使用 `quack`
3 鸭子仍在飞,来自默认实现
4 quack 来自 Duck
5 speak 不再使用 SpeakingAbility 的默认实现

5.8. 扩展特质

5.8.1. 简单继承

特质可以扩展另一个特质,在这种情况下,您必须使用 extends 关键字

trait Named {
    String name                                     (1)
}
trait Polite extends Named {                        (2)
    String introduce() { "Hello, I am $name" }      (3)
}
class Person implements Polite {}
def p = new Person(name: 'Alice')                   (4)
assert p.introduce() == 'Hello, I am Alice'         (5)
1 Named 特质定义了一个 name 属性
2 Polite 特质**扩展**了 Named 特质
3 Polite 添加了一个新方法,该方法可以访问超特质的 name 属性
4 从实现 PolitePerson 类中可以访问 name 属性
5 以及 introduce 方法

5.8.2. 多重继承

或者,一个特质可以扩展多个特质。在这种情况下,所有超特质都必须在 implements 子句中声明

trait WithId {                                      (1)
    Long id
}
trait WithName {                                    (2)
    String name
}
trait Identified implements WithId, WithName {}     (3)
1 WithId 特质定义了 id 属性
2 WithName 特质定义了 name 属性
3 Identified 是一个继承了 WithIdWithName 的特质

5.9. 鸭子类型和特质

5.9.1. 动态代码

特质可以像普通的 Groovy 类一样调用任何动态代码。这意味着您可以在方法体中调用应该存在于实现类中的方法,而无需在接口中显式声明它们。这意味着特质与鸭子类型完全兼容

trait SpeakingDuck {
    String speak() { quack() }                      (1)
}
class Duck implements SpeakingDuck {
    String methodMissing(String name, args) {
        "${name.capitalize()}!"                     (2)
    }
}
def d = new Duck()
assert d.speak() == 'Quack!'                        (3)
1 SpeakingDuck 期望 quack 方法被定义
2 Duck 类确实使用 *methodMissing* 实现了该方法
3 调用 speak 方法会触发对 quack 的调用,该调用由 methodMissing 处理

5.9.2. 特质中的动态方法

特质也可以实现 MOP 方法,如 methodMissingpropertyMissing,在这种情况下,实现类将继承特质的行为,如下例所示

trait DynamicObject {                               (1)
    private Map props = [:]
    def methodMissing(String name, args) {
        name.toUpperCase()
    }
    def propertyMissing(String name) {
        props.get(name)
    }
    void setProperty(String name, Object value) {
        props.put(name, value)
    }
}

class Dynamic implements DynamicObject {
    String existingProperty = 'ok'                  (2)
    String existingMethod() { 'ok' }                (3)
}
def d = new Dynamic()
assert d.existingProperty == 'ok'                   (4)
assert d.foo == null                                (5)
d.foo = 'bar'                                       (6)
assert d.foo == 'bar'                               (7)
assert d.existingMethod() == 'ok'                   (8)
assert d.someMethod() == 'SOMEMETHOD'               (9)
1 创建一个实现多个 MOP 方法的特质
2 Dynamic 类定义了一个属性
3 Dynamic 类定义了一个方法
4 调用现有属性将调用 Dynamic 中的方法
5 调用不存在的属性将调用特质中的方法
6 将调用特质上定义的 setProperty
7 将调用特质上定义的 getProperty
8 调用 Dynamic 上的现有方法
9 但由于特质的 methodMissing,调用了不存在的方法

5.10. 多重继承冲突

5.10.1. 默认冲突解决

一个类可以实现多个特质。如果某个特质定义了一个与另一个特质中具有相同签名的方法,则会发生冲突

trait A {
    String exec() { 'A' }               (1)
}
trait B {
    String exec() { 'B' }               (2)
}
class C implements A,B {}               (3)
1 特质 A 定义了一个名为 exec 的方法,返回一个 String
2 特质 B 定义了完全相同的方法
3 C 实现这两个特质

在这种情况下,默认行为是 implements 子句中**最后声明的特质**中的方法胜出。这里,BA 之后声明,因此将选择 B 中的方法

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

5.10.2. 用户冲突解决

如果这不是您想要的行为,您可以使用 Trait.super.foo 语法显式选择要调用的方法。在上面的示例中,我们可以通过这样编写来确保调用特质 A 中的方法

class C implements A,B {
    String exec() { A.super.exec() }    (1)
}
def c = new C()
assert c.exec() == 'A'                  (2)
1 显式调用特质 A 中的 exec
2 调用 A 中的版本,而不是使用默认解决方案(即 B 中的版本)

5.11. 特质的运行时实现

5.11.1. 运行时实现特质

Groovy 还支持在运行时动态实现特质。它允许您使用特质“装饰”现有对象。例如,让我们从这个特质和以下类开始

trait Extra {
    String extra() { "I'm an extra method" }            (1)
}
class Something {                                       (2)
    String doSomething() { 'Something' }                (3)
}
1 Extra 特质定义了一个 extra 方法
2 Something 类**不**实现 Extra 特质
3 Something 只定义了一个方法 doSomething

然后如果我们这样做

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

对 extra 的调用将失败,因为 Something 没有实现 Extra。可以使用以下语法在运行时完成

def s = new Something() as Extra                        (1)
s.extra()                                               (2)
s.doSomething()                                         (3)
1 使用 **as** 关键字在**运行时**将对象强制转换为特质
2 然后可以在对象上调用 extra
3 并且 doSomething 仍然可以调用
当将对象强制转换为特质时,操作的结果不是同一个实例。可以保证被强制转换的对象将实现特质**和**原始对象实现的接口,但结果将**不是**原始类的实例。

5.11.2. 同时实现多个特质

如果您需要同时实现多个特质,可以使用 withTraits 方法而不是 as 关键字

trait A { void methodFromA() {} }
trait B { void methodFromB() {} }

class C {}

def c = new C()
c.methodFromA()                     (1)
c.methodFromB()                     (2)
def d = c.withTraits A, B           (3)
d.methodFromA()                     (4)
d.methodFromB()                     (5)
1 调用 methodFromA 将失败,因为 C 没有实现 A
2 调用 methodFromB 将失败,因为 C 没有实现 B
3 withTrait 将把 c 封装成实现 AB 的对象
4 methodFromA 现在会通过,因为 d 实现了 A
5 methodFromB 现在也会通过,因为 d 也实现了 B
当将对象强制转换为多个特质时,操作的结果不是同一个实例。可以保证被强制转换的对象将实现特质**和**原始对象实现的接口,但结果将**不是**原始类的实例。

5.12. 链式行为

Groovy 支持*可堆叠特质*的概念。其思想是如果当前特质无法处理消息,则将消息委托给另一个特质。为了说明这一点,让我们想象一个消息处理程序接口,如下所示

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

然后您可以通过应用小行为来组成消息处理程序。例如,让我们以特质的形式定义一个默认处理程序

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

然后任何类都可以通过实现特质来继承默认处理程序的行为

class SimpleHandler implements DefaultHandler {}

现在,除了默认处理程序之外,如果您还想记录所有消息怎么办?一个选项是这样编写

class SimpleHandlerWithLogging implements DefaultHandler {
    void on(String message, Map payload) {                                  (1)
        println "Seeing $message with payload $payload"                     (2)
        DefaultHandler.super.on(message, payload)                           (3)
    }
}
1 显式实现 on 方法
2 执行日志记录
3 通过委托给 DefaultHandler 特质继续

这有效,但这种方法有缺点

  1. 日志逻辑绑定到一个“具体”处理程序

  2. 我们在 on 方法中显式引用了 DefaultHandler,这意味着如果我们碰巧更改了我们的类实现的特质,代码将会损坏

作为替代方案,我们可以编写另一个特质,其职责仅限于日志记录

trait LoggingHandler implements MessageHandler {                            (1)
    void on(String message, Map payload) {
        println "Seeing $message with payload $payload"                     (2)
        super.on(message, payload)                                          (3)
    }
}
1 日志处理程序本身就是一个处理程序
2 打印它收到的消息
3 然后 super 使它将调用委托给链中的下一个特质

然后我们的类可以重写为这样

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

它将打印

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

由于优先级规则意味着 LoggerHandler 胜出,因为它最后声明,因此对 on 的调用将使用 LoggingHandler 中的实现。但后者调用了 super,这意味着链中的下一个特质。在这里,下一个特质是 DefaultHandler,因此**两者**都将被调用

如果我们添加第三个处理程序,其职责是处理以 say 开头的消息,这种方法的兴趣就变得更加明显

trait SayHandler implements MessageHandler {
    void on(String message, Map payload) {
        if (message.startsWith("say")) {                                    (1)
            println "I say ${message - 'say'}!"
        } else {
            super.on(message, payload)                                      (2)
        }
    }
}
1 处理程序特定的前置条件
2 如果未满足前置条件,则将消息传递给链中的下一个处理程序

然后我们的最终处理程序看起来像这样

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

这意味着

  • 消息将首先通过日志处理程序

  • 日志处理程序调用 super,它将委托给下一个处理程序,即 SayHandler

  • 如果消息以 say 开头,则处理程序消耗消息

  • 如果没有,say 处理程序委托给链中的下一个处理程序

这种方法非常强大,因为它允许您编写彼此不了解的处理程序,但仍允许您按所需的顺序组合它们。例如,如果我们执行代码,它将打印

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

但如果我们将日志处理程序移到链中的第二个,输出就会不同

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

打印

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

原因是现在,由于 SayHandler 在不调用 super 的情况下消耗消息,日志处理程序不再被调用。

5.12.1. 特质内部 super 的语义

如果一个类实现了多个特质,并且发现对不合格的 super 的调用,那么

  1. 如果类实现了另一个特质,则调用委托给链中的下一个特质

  2. 如果链中没有剩余特质,则 super 指的是实现类的超类(*this*)

例如,由于这种行为,可以装饰最终类

trait Filtering {                                       (1)
    StringBuilder append(String str) {                  (2)
        def subst = str.replace('o','')                 (3)
        super.append(subst)                             (4)
    }
    String toString() { super.toString() }              (5)
}
def sb = new StringBuilder().withTraits Filtering       (6)
sb.append('Groovy')
assert sb.toString() == 'Grvy'                          (7)
1 定义一个名为 Filtering 的特质,假定在运行时应用于 StringBuilder
2 重新定义 append 方法
3 从字符串中删除所有 'o'
4 然后委托给 super
5 如果调用 toString,则委托给 super.toString
6 StringBuilder 实例上运行时实现 Filtering 特质
7 已追加的字符串不再包含字母 o

在此示例中,当遇到 super.append 时,目标对象没有实现其他特质,因此调用的方法是原始的 append 方法,即 StringBuilder 中的方法。对 toString 也使用了相同的技巧,以便生成的代理对象的字符串表示委托给 StringBuilder 实例的 toString

5.13. 高级功能

5.13.1. SAM 类型强制转换

如果一个特质定义了一个抽象方法,它就可以作为 SAM(单一抽象方法)类型强制转换的候选。例如,想象以下特质

trait Greeter {
    String greet() { "Hello $name" }        (1)
    abstract String getName()               (2)
}
1 greet 方法不是抽象的,并调用抽象方法 getName
2 getName 是一个抽象方法

由于 getNameGreeter 特质中的*单一抽象方法*,您可以这样编写

Greeter greeter = { 'Alice' }               (1)
1 闭包“成为” getName 单一抽象方法的实现

甚至

void greet(Greeter g) { println g.greet() } (1)
greet { 'Alice' }                           (2)
1 greet 方法接受 SAM 类型 Greeter 作为参数
2 我们可以直接使用闭包调用它

5.13.2. 与 Java 8 默认方法的区别

在 Java 8 中,接口可以有方法的默认实现。如果一个类实现了一个接口并且没有为默认方法提供实现,那么将选择接口中的实现。特质的行为相同,但有一个主要区别:如果类在其接口列表中声明了特质**并且**它没有提供实现**即使**超类提供了实现,特质中的实现也**总是**被使用。

此功能可用于以非常精确的方式组合行为,以防您想要覆盖已实现方法的行为。

为了说明这个概念,让我们从这个简单的例子开始

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() { /* ... */ }
}

在此示例中,我们创建了一个简单的测试用例,它使用两个属性(*config* 和 *shell*),并在多个测试方法中使用它们。现在想象一下,您想以另一种不同的编译器配置来测试相同的内容。一个选项是创建 SomeTest 的子类

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

它有效,但是如果您实际上有多个测试类,并且您想为所有这些测试类测试新配置怎么办?那么您必须为每个测试类创建一个不同的子类

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

然后您看到的是两个测试的 setup 方法是相同的。那么,这个想法是创建一个特质

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

然后在子类中使用它

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

它将极大地减少样板代码,并减少了如果我们要更改设置代码而忘记更改它的风险。即使 setup 已在超类中实现,由于测试类在其接口列表中声明了特质,因此行为将从特质实现中借用!

此功能在您无法访问超类源代码时特别有用。它可用于模拟方法或强制子类中方法的特定实现。它允许您重构代码,将重写逻辑保留在单个特质中,并通过实现它来继承新行为。当然,替代方案是在您使用新代码的**每个**地方覆盖该方法。

值得注意的是,如果您使用运行时特质,特质中的方法**总是**优先于代理对象中的方法
class Person {
    String name                                         (1)
}
trait Bob {
    String getName() { 'Bob' }                          (2)
}

def p = new Person(name: 'Alice')
assert p.name == 'Alice'                                (3)
def p2 = p as Bob                                       (4)
assert p2.name == 'Bob'                                 (5)
1 Person 类定义了一个 name 属性,该属性导致一个 getName 方法
2 Bob 是一个特质,它将 getName 定义为返回 Bob
3 默认对象将返回 *Alice*
4 p2 在运行时将 p 强制转换为 Bob
5 getName 返回 *Bob*,因为 getName 是从**特质**中获取的
再次强调,不要忘记动态特质强制转换返回一个不同的对象,该对象只实现了原始接口和特质。

5.14. 与 mixin 的区别

与 Groovy 中可用的 mixin 存在一些概念上的差异。请注意,我们讨论的是运行时 mixin,而不是已弃用并倾向于使用特质的 @Mixin 注解。

首先,特质中定义的方法在字节码中可见

  • 在内部,特质表示为一个接口(没有默认或静态方法)和几个辅助类

  • 这意味着实现特质的对象实际上实现了一个*接口*

  • 这些方法在 Java 中可见

  • 它们与类型检查和静态编译兼容

相反,通过 mixin 添加的方法仅在运行时可见

class A { String methodFromA() { 'A' } }        (1)
class B { String methodFromB() { 'B' } }        (2)
A.metaClass.mixin B                             (3)
def o = new A()
assert o.methodFromA() == 'A'                   (4)
assert o.methodFromB() == 'B'                   (5)
assert o instanceof A                           (6)
assert !(o instanceof B)                        (7)
1 A 定义了 methodFromA
2 B 定义了 methodFromB
3 将 B 混入 A
4 我们可以调用 methodFromA
5 我们也可以调用 methodFromB
6 对象是 A 的实例
7 但它**不是** B 的实例

最后一点实际上非常重要,并说明了 mixin 优于特质的一个地方:实例**未**修改,因此如果您将某个类混入另一个类,则不会生成第三个类,并且响应 A 的方法即使混入后也将继续响应 A。

5.15. 静态方法、属性和字段

以下说明需要谨慎。静态成员支持仍在进行中,并且仍处于实验阶段。以下信息仅适用于 4.0.28。

可以在特质中定义静态方法,但这有许多限制

  • 带有静态方法的特质不能静态编译或类型检查。所有静态方法、属性和字段都动态访问(这是 JVM 的限制)。

  • 静态方法不会出现在为每个特质生成的接口中。

  • 特质被解释为实现类的*模板*,这意味着每个实现类都将拥有自己的静态方法、属性和字段。因此,特质上声明的静态成员不属于 Trait,而是属于其实现类。

  • 通常不应混合具有相同签名的静态方法和实例方法。应用特质的正常规则适用(包括多重继承冲突解决)。如果选择的方法是静态的,但某个实现的特质具有实例变体,则会发生编译错误。如果选择的方法是实例变体,则静态变体将被忽略(在这种情况下,行为类似于 Java 接口中的静态方法)。

让我们从一个简单的例子开始

trait TestHelper {
    public static boolean CALLED = false        (1)
    static void init() {                        (2)
        CALLED = true                           (3)
    }
}
class Foo implements TestHelper {}
Foo.init()                                      (4)
assert Foo.TestHelper__CALLED                   (5)
1 静态字段在特质中声明
2 特质中也声明了一个静态方法
3 静态字段在特质*内部*更新
4 一个静态方法 *init* 可用于实现类
5 静态字段*重新映射*以避免菱形问题

和往常一样,不建议使用公共字段。无论如何,如果您想要这样做,您必须了解以下代码将失败

Foo.CALLED = true

因为特质本身*没有*名为 *CALLED* 的静态字段。同样,如果您有两个不同的实现类,每个类都会获得一个不同的静态字段

class Bar implements TestHelper {}              (1)
class Baz implements TestHelper {}              (2)
Bar.init()                                      (3)
assert Bar.TestHelper__CALLED                   (4)
assert !Baz.TestHelper__CALLED                  (5)
1 Bar 实现了该特质
2 Baz 也实现了该特质
3 init 仅在 Bar 上调用
4 Bar 上的静态字段 CALLED 已更新
5 Baz 上的静态字段 CALLED 没有更新,因为它**不同**

5.16. 状态继承的陷阱

我们已经看到特质是有状态的。特质可以定义字段或属性,但当一个类实现一个特质时,它会按特质获取这些字段/属性。所以请考虑以下示例

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

该特质定义了两个属性 xy,以及一个 sum 方法。现在让我们创建一个实现该特质的类

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

调用 f 的结果是 3,因为 f 委托给特质中的 sum,而 sum 有状态。但如果我们这样写呢?

class Elem implements IntCouple {
    int x = 3                                       (1)
    int y = 4                                       (2)
    int f() { sum() }                               (3)
}
def elem = new Elem()
1 覆盖属性 x
2 覆盖属性 y
3 从特质调用 sum

如果您调用 elem.f(),预期的输出是什么?实际上是

assert elem.f() == 3

原因是 sum 方法访问特质的*字段*。因此,它使用特质中定义的 xy 值。如果您想使用实现类中的值,则需要使用 getter 和 setter 解引用字段,如下面的最后一个示例所示

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

5.17. 自类型

5.17.1. 特质上的类型约束

有时您会想要编写一个只能应用于某些类型的特质。例如,您可能希望将特质应用于一个扩展您无法控制的另一个类的类,并且仍然能够调用这些方法。为了说明这一点,让我们从这个例子开始

class CommunicationService {
    static void sendMessage(String from, String to, String message) {       (1)
        println "$from sent [$message] to $to"
    }
}

class Device { String id }                                                  (2)

trait Communicating {
    void sendMessage(Device to, String message) {
        CommunicationService.sendMessage(id, to.id, message)                (3)
    }
}

class MyDevice extends Device implements Communicating {}                   (4)

def bob = new MyDevice(id:'Bob')
def alice = new MyDevice(id:'Alice')
bob.sendMessage(alice,'secret')                                             (5)
1 一个 Service 类,超出您的控制范围(在库中等),定义了一个 sendMessage 方法
2 一个 Device 类,超出您的控制范围(在库中等)
3 为可以调用服务的设备定义一个通信特质
4 MyDevice 定义为通信设备
5 调用了特质中的方法,并解析了 id

在这里,很明显 Communicating 特质只能应用于 Device。然而,没有明确的契约来表明这一点,因为特质不能扩展类。然而,代码编译并运行得非常好,因为特质方法中的 id 将动态解析。问题在于,没有任何东西阻止特质应用于**不是** Device 的任何类。任何具有 id 的类都可以工作,而任何没有 id 属性的类都会导致运行时错误。

如果您想在特质上启用类型检查或应用 @CompileStatic,问题会更加复杂:因为特质不知道自己是 Device,类型检查器会抱怨说它找不到 id 属性。

一种可能性是在特质中显式添加一个 getId 方法,但这并不能解决所有问题。如果一个方法需要 this 作为参数,并且实际上要求它是一个 Device 怎么办?

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

如果您想在特质中调用 this,那么您将需要显式地将 this 转换为 Device。这很快就会变得不可读,到处都是显式转换为 this

5.17.2. @SelfType 注解

为了使此契约显式化,并使类型检查器了解*自身的类型*,Groovy 提供了 @SelfType 注解,它将

  • 允许您声明实现此特质的类必须继承或实现的类型

  • 如果不满足这些类型约束,则抛出编译时错误

因此,在我们之前的示例中,我们可以使用 @groovy.transform.SelfType 注解来修复特质

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

现在,如果您尝试在**不是**设备 的类上实现此特质,则会发生编译时错误

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

错误将是

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

总之,自类型是一种强大的方式,可以在不直接在特质中声明契约或到处使用强制转换的情况下声明对特质的约束,从而保持关注点分离的紧密性。

5.17.3. 与 Sealed 注解(孵化中)的区别

@Sealed@SelfType 都限制使用特质的类,但方式正交。考虑以下示例

interface HasHeight { double getHeight() }
interface HasArea { double getArea() }

@SelfType([HasHeight, HasArea])                       (1)
@Sealed(permittedSubclasses=[UnitCylinder,UnitCube])  (2)
trait HasVolume {
    double getVolume() { height * area }
}

final class UnitCube implements HasVolume, HasHeight, HasArea {
    // for the purposes of this example: h=1, w=1, l=1
    double height = 1d
    double area = 1d
}

final class UnitCylinder implements HasVolume, HasHeight, HasArea {
    // for the purposes of this example: h=1, diameter=1
    // radius=diameter/2, area=PI * r^2
    double height = 1d
    double area = Math.PI * 0.5d**2
}

assert new UnitCube().volume == 1d
assert new UnitCylinder().volume == 0.7853981633974483d
1 所有使用 HasVolume 特质的类都必须实现或扩展 HasHeightHasArea
2 只有 UnitCubeUnitCylinder 可以使用该特质

对于单个类实现特质的退化情况,例如

final class Foo implements FooTrait {}

那么,要么

@SelfType(Foo)
trait FooTrait {}

或者

@Sealed(permittedSubclasses='Foo') (1)
trait FooTrait {}
1 或者如果 FooFooTrait 在同一个源文件中,则只需 @Sealed

可以表达此约束。通常,前者更受青睐。

5.18. 限制

5.18.1. 与 AST 转换的兼容性

特质与 AST 转换不正式兼容。其中一些,如 @CompileStatic 将应用于特质本身(而不是实现类),而另一些将同时应用于实现类和特质。绝对不能保证 AST 转换将在特质上像在常规类上一样运行,因此请自行承担风险!

5.18.2. 前缀和后缀操作

在特质中,如果前缀和后缀操作更新特质的字段,则不允许它们

trait Counting {
    int x
    void inc() {
        x++                             (1)
    }
    void dec() {
        --x                             (2)
    }
}
class Counter implements Counting {}
def c = new Counter()
c.inc()
1 x 在特质中定义,不允许后缀增量
2 x 在特质中定义,不允许前缀减量

一种解决方法是使用 += 运算符。

6. 记录类(孵化中)

记录类,简称*记录*,是一种特殊的类,用于建模普通数据聚合。它们提供了一种紧凑的语法,比普通类更少繁琐。Groovy 已经有像 @Immutable@Canonical 这样的 AST 转换,它们已经大大减少了繁琐,但记录已在 Java 中引入,Groovy 中的记录类旨在与 Java 记录类对齐。

例如,假设我们想创建一个 Message 记录来表示一封电子邮件消息。为了本示例的目的,让我们将此类消息简化为仅包含一个*发件人*电子邮件地址、一个*收件人*电子邮件地址和一个消息*正文*。我们可以如下定义这样一个记录

record Message(String from, String to, String body) { }

我们将以与普通类相同的方式使用记录类,如下所示

def msg = new Message('me@myhost.com', 'you@yourhost.net', 'Hello!')
assert msg.toString() == 'Message[from=me@myhost.com, to=you@yourhost.net, body=Hello!]'

精简的仪式让我们免去了定义显式字段、getter 以及 toStringequalshashCode 方法。事实上,它等同于以下大致的简写

final class Message extends Record {
    private final String from
    private final String to
    private final String body
    private static final long serialVersionUID = 0

    /* constructor(s) */

    final String toString() { /*...*/ }

    final boolean equals(Object other) { /*...*/ }

    final int hashCode() { /*...*/ }

    String from() { from }
    // other getters ...
}

请注意记录 getter 的特殊命名约定。它们与字段同名(而不是通常的 JavaBean 约定,即首字母大写并带有“get”前缀)。记录通常不提及其字段或属性,而是使用*组件*一词。因此,我们的 Message 记录具有 fromtobody 组件。

与 Java 中一样,您可以通过编写自己的方法来覆盖通常隐式提供的方法

record Point3D(int x, int y, int z) {
    String toString() {
        "Point3D[coords=$x,$y,$z]"
    }
}

assert new Point3D(10, 20, 30).toString() == 'Point3D[coords=10,20,30]'

您还可以以正常方式将泛型与记录一起使用。例如,考虑以下 Coord 记录定义

record Coord<T extends Number>(T v1, T v2){
    double distFromOrigin() { Math.sqrt(v1()**2 + v2()**2 as double) }
}

它可以如下使用

def r1 = new Coord<Integer>(3, 4)
assert r1.distFromOrigin() == 5
def r2 = new Coord<Double>(6d, 2.5d)
assert r2.distFromOrigin() == 6.5d

6.1. 记录的特殊功能

6.1.1. 紧凑构造函数

记录有一个隐式构造函数。这可以通过提供您自己的构造函数来以正常方式覆盖——如果您这样做,您需要确保设置所有字段。但是,为了简洁起见,可以使用紧凑构造函数语法,其中省略了普通构造函数的参数声明部分。对于这种特殊情况,仍然提供正常的隐式构造函数,但通过紧凑构造函数定义中提供的语句进行增强

public record Warning(String message) {
    public Warning {
        Objects.requireNonNull(message)
        message = message.toUpperCase()
    }
}

def w = new Warning('Help')
assert w.message() == 'HELP'

6.1.2. 可序列化性

Groovy *原生*记录遵循适用于 Java 记录的特殊约定以实现可序列化性。Groovy *类记录*(下文讨论)遵循普通的 Java 类序列化约定。

6.2. Groovy 增强

6.2.1. 参数默认值

Groovy 支持构造函数参数的默认值。此功能也适用于记录,如下面的记录定义所示,其中 ycolor 具有默认值

record ColoredPoint(int x, int y = 0, String color = 'white') {}

当省略参数(从右侧省略一个或多个参数)时,它们将被替换为其默认值,如下例所示

assert new ColoredPoint(5, 5, 'black').toString() == 'ColoredPoint[x=5, y=5, color=black]'
assert new ColoredPoint(5, 5).toString() == 'ColoredPoint[x=5, y=5, color=white]'
assert new ColoredPoint(5).toString() == 'ColoredPoint[x=5, y=0, color=white]'

此处理遵循 Groovy 构造函数默认参数的正常约定,本质上是自动提供具有以下签名的构造函数

ColoredPoint(int, int, String)
ColoredPoint(int, int)
ColoredPoint(int)

命名参数也可以使用(默认值也适用于此处)

assert new ColoredPoint(x: 5).toString() == 'ColoredPoint[x=5, y=0, color=white]'
assert new ColoredPoint(x: 0, y: 5).toString() == 'ColoredPoint[x=0, y=5, color=white]'

您可以禁用默认参数处理,如下所示

@TupleConstructor(defaultsMode=DefaultsMode.OFF)
record ColoredPoint2(int x, int y, String color) {}
assert new ColoredPoint2(4, 5, 'red').toString() == 'ColoredPoint2[x=4, y=5, color=red]'

这将生成一个与 Java 默认值相同的单个构造函数。在这种情况下,如果您省略参数,将是错误。

您可以强制所有属性都具有默认值,如下所示

@TupleConstructor(defaultsMode=DefaultsMode.ON)
record ColoredPoint3(int x, int y = 0, String color = 'white') {}
assert new ColoredPoint3(y: 5).toString() == 'ColoredPoint3[x=0, y=5, color=white]'

任何没有显式初始值的属性/字段都将获得该参数类型的默认值(null,或原始类型的零/false)。

深入了解

我们之前描述了一个 Message 记录并显示了它的近似等效项。实际上,Groovy 经历了一个中间阶段,其中 record 关键字被 class 关键字和附带的 @RecordType 注解取代

@RecordType
class Message {
    String from
    String to
    String body
}

然后 @RecordType 本身被作为*元注解*(注解收集器)处理,并扩展为其组成子注解,例如 @TupleConstructor@POJO@RecordBase 等。这在某种程度上是一个实现细节,通常可以忽略。但是,如果您希望自定义或配置记录实现,您可能希望回退到 @RecordType 样式或使用其中一个组成子注解增强您的记录类。

6.2.2. 声明式 toString 定制

与 Java 一样,您可以通过编写自己的方法来定制记录的 toString 方法。如果您喜欢更声明式的风格,您可以选择使用 Groovy 的 @ToString 转换来覆盖默认的记录 toString。例如,您可以如下定义一个三维点记录

package threed

import groovy.transform.ToString

@ToString(ignoreNulls=true, cache=true, includeNames=true,
          leftDelimiter='[', rightDelimiter=']', nameValueSeparator='=')
record Point(Integer x, Integer y, Integer z=null) { }

assert new Point(10, 20).toString() == 'threed.Point[x=10, y=20]'

我们通过包含包名(记录默认排除)和缓存 toString 值来定制 toString,因为对于此不可变记录,它不会改变。我们还忽略了空值(我们定义中 z 的默认值)。

我们可以为二维点定义类似的定义

package twod

import groovy.transform.ToString

@ToString(ignoreNulls=true, cache=true, includeNames=true,
          leftDelimiter='[', rightDelimiter=']', nameValueSeparator='=')
record Point(Integer x, Integer y) { }

assert new Point(10, 20).toString() == 'twod.Point[x=10, y=20]'

我们在这里可以看到,如果没有包名,它将与我们之前的示例具有相同的 toString。

6.2.3. 获取记录组件值的列表

我们可以将记录的组件值作为列表获取,如下所示

record Point(int x, int y, String color) { }

def p = new Point(100, 200, 'green')
def (x, y, c) = p.toList()
assert x == 100
assert y == 200
assert c == 'green'

您可以使用 @RecordOptions(toList=false) 禁用此功能。

6.2.4. 获取记录组件值的映射

我们可以将记录的组件值作为映射获取,如下所示

record Point(int x, int y, String color) { }

def p = new Point(100, 200, 'green')
assert p.toMap() == [x: 100, y: 200, color: 'green']

您可以使用 @RecordOptions(toMap=false) 禁用此功能。

6.2.5. 获取记录中的组件数量

我们可以像这样获取记录中的组件数量

record Point(int x, int y, String color) { }

def p = new Point(100, 200, 'green')
assert p.size() == 3

您可以使用 @RecordOptions(size=false) 禁用此功能。

6.2.6. 从记录中获取第 n 个组件

我们可以使用 Groovy 的普通位置索引来获取记录中的特定组件,如下所示

record Point(int x, int y, String color) { }

def p = new Point(100, 200, 'green')
assert p[1] == 200

您可以使用 @RecordOptions(getAt=false) 禁用此功能。

6.3. 可选的 Groovy 功能

6.3.1. 复制

复制具有某些组件已更改的记录可能很有用。这可以使用可选的 copyWith 方法来完成,该方法接受命名参数。记录组件根据提供的参数设置。对于未提及的组件,使用原始记录组件的(浅)副本。以下是您如何使用 copyWith 处理 Fruit 记录的示例

@RecordOptions(copyWith=true)
record Fruit(String name, double price) {}
def apple = new Fruit('Apple', 11.6)
assert 'Apple' == apple.name()
assert 11.6 == apple.price()

def orange = apple.copyWith(name: 'Orange')
assert orange.toString() == 'Fruit[name=Orange, price=11.6]'

可以通过将 RecordOptions#copyWith 注解属性设置为 false 来禁用 copyWith 功能。

6.3.2. 深度不变性

与 Java 一样,记录默认提供浅层不变性。Groovy 的 @Immutable 转换对一系列可变数据类型执行防御性复制。记录可以利用此防御性复制来获得深度不变性,如下所示

@ImmutableProperties
record Shopping(List items) {}

def items = ['bread', 'milk']
def shop = new Shopping(items)
items << 'chocolate'
assert shop.items() == ['bread', 'milk']

这些示例说明了 Groovy 记录功能提供三个便利级别的原理

  • 使用 record 关键字以实现最大简洁性

  • 支持使用声明式注解进行低仪式定制

  • 在需要完全控制时允许正常方法实现

6.3.3. 将记录组件作为类型化元组获取

您可以将记录的组件作为类型化元组获取

import groovy.transform.*

@RecordOptions(components=true)
record Point(int x, int y, String color) { }

@CompileStatic
def method() {
    def p1 = new Point(100, 200, 'green')
    def (int x1, int y1, String c1) = p1.components()
    assert x1 == 100
    assert y1 == 200
    assert c1 == 'green'

    def p2 = new Point(10, 20, 'blue')
    def (x2, y2, c2) = p2.components()
    assert x2 * 10 == 100
    assert y2 ** 2 == 400
    assert c2.toUpperCase() == 'BLUE'

    def p3 = new Point(1, 2, 'red')
    assert p3.components() instanceof Tuple3
}

method()

Groovy 有数量有限的 TupleN 类。如果您的记录中包含大量组件,您可能无法使用此功能。

6.4. 与 Java 的其他区别

Groovy 支持创建*类记录*以及原生记录。类记录不扩展 Java 的 Record 类,因此 Java 不会将此类视为记录,但它们将具有相似的属性。

@RecordOptions 注解(@RecordType 的一部分)支持一个 mode 注解属性,它可以取三个值之一(AUTO 是默认值)

NATIVE

生成与 Java 相同的类。在 JDK16 之前的 JDK 上编译时会产生错误。

EMULATE

为所有 JDK 版本生成类记录。

AUTO

为 JDK16+ 生成原生记录,否则模拟记录。

您是使用 record 关键字还是 @RecordType 注解与模式无关。

7. 密封层次结构(孵化中)

密封类、接口和特质限制哪些子类可以扩展/实现它们。在密封类出现之前,类层次结构设计者有两个主要选择

  • 使类为 final 以禁止扩展。

  • 使类为 public 且非 final 以允许任何人扩展。

密封类在这些“全有或全无”的选择之间提供了一个中间地带。

密封类也比以前用于实现中间地带的其他技巧更灵活。例如,对于类层次结构,protected 和 package-private 等访问修饰符在一定程度上限制了继承层次结构,但通常以牺牲这些层次结构的灵活使用为代价。

密封层次结构在已知类、接口和特质层次结构中提供完全继承,但在层次结构之外禁用或仅提供受控继承。

例如,假设我们想创建一个只包含圆形和正方形的形状层次结构。我们还希望一个形状接口能够引用我们层次结构中的实例。我们可以如下创建层次结构

sealed interface ShapeI permits Circle,Square { }
final class Circle implements ShapeI { }
final class Square implements ShapeI { }

Groovy 也支持另一种注解语法。我们认为关键字样式更好,但如果您的编辑器尚未支持 Groovy 4,您可能会选择注解样式。

@Sealed(permittedSubclasses=[Circle,Square]) interface ShapeI { }
final class Circle implements ShapeI { }
final class Square implements ShapeI { }

我们可以有一个 ShapeI 类型的引用,由于 permits 子句,它可以指向 CircleSquare,并且由于我们的类是 final,我们知道将来不会向我们的层次结构添加其他类。至少不会在不更改 permits 子句和重新编译的情况下。

一般来说,我们可能希望立即锁定类层次结构的某些部分,就像我们在这里所做的那样,我们将子类标记为 final,但有时我们可能希望允许进一步受控的继承。

sealed class Shape permits Circle,Polygon,Rectangle { }

final class Circle extends Shape { }

class Polygon extends Shape { }
non-sealed class RegularPolygon extends Polygon { }
final class Hexagon extends Polygon { }

sealed class Rectangle extends Shape permits Square{ }
final class Square extends Rectangle { }
<点击查看备用注解语法>
@Sealed(permittedSubclasses=[Circle,Polygon,Rectangle]) class Shape { }

final class Circle extends Shape { }

class Polygon extends Shape { }
@NonSealed class RegularPolygon extends Polygon { }
final class Hexagon extends Polygon { }

@Sealed(permittedSubclasses=Square) class Rectangle extends Shape { }
final class Square extends Rectangle { }

 
在此示例中,我们允许 Shape 的子类是 CirclePolygonRectangleCirclefinal,因此层次结构的该部分无法扩展。Polygon 隐式为非密封,RegularPolygon 显式标记为 non-sealed。这意味着我们的层次结构对子类的任何进一步扩展开放,如 Polygon → RegularPolygonRegularPolygon → Hexagon 所示。Rectangle 本身是密封的,这意味着层次结构的该部分可以扩展,但只能以受控方式进行(只允许 Square)。

密封类对于创建需要包含实例特定数据的枚举样式的相关类非常有用。例如,我们可能有以下枚举

enum Weather { Rainy, Cloudy, Sunny }
def forecast = [Weather.Rainy, Weather.Sunny, Weather.Cloudy]
assert forecast.toString() == '[Rainy, Sunny, Cloudy]'

但我们现在还希望向天气预报添加天气特定实例数据。我们可以如下更改我们的抽象

sealed abstract class Weather { }
@Immutable(includeNames=true) class Rainy extends Weather { Integer expectedRainfall }
@Immutable(includeNames=true) class Sunny extends Weather { Integer expectedTemp }
@Immutable(includeNames=true) class Cloudy extends Weather { Integer expectedUV }
def forecast = [new Rainy(12), new Sunny(35), new Cloudy(6)]
assert forecast.toString() == '[Rainy(expectedRainfall:12), Sunny(expectedTemp:35), Cloudy(expectedUV:6)]'

密封层次结构在指定代数或抽象数据类型(ADT)时也很有用,如下例所示

import groovy.transform.*

sealed interface Tree<T> {}
@Singleton final class Empty implements Tree {
    String toString() { 'Empty' }
}
@Canonical final class Node<T> implements Tree<T> {
    T value
    Tree<T> left, right
}

Tree<Integer> tree = new Node<>(42, new Node<>(0, Empty.instance, Empty.instance), Empty.instance)
assert tree.toString() == 'Node(42, Node(0, Empty, Empty), Empty)'

密封层次结构与记录配合得很好,如下例所示

sealed interface Expr {}
record ConstExpr(int i) implements Expr {}
record PlusExpr(Expr e1, Expr e2) implements Expr {}
record MinusExpr(Expr e1, Expr e2) implements Expr {}
record NegExpr(Expr e) implements Expr {}

def threePlusNegOne = new PlusExpr(new ConstExpr(3), new NegExpr(new ConstExpr(1)))
assert threePlusNegOne.toString() == 'PlusExpr[e1=ConstExpr[i=3], e2=NegExpr[e=ConstExpr[i=1]]]'

7.1. 与 Java 的区别

  • Java 不为密封类的子类提供默认修饰符,并要求指定 finalsealednon-sealed 之一。Groovy 默认为 *non-sealed*,但如果您愿意,仍然可以使用 non-sealed/@NonSealed。我们预计样式检查工具 CodeNarc 最终会有一个查找 non-sealed 存在的规则,因此希望采用更严格样式的开发人员将能够使用 CodeNarc 和该规则。

  • 目前,Groovy 不检查 permittedSubclasses 中提及的所有类在编译时都可用并与基本密封类一起编译。这可能会在 Groovy 的未来版本中改变。

Groovy 支持将类注解为密封以及“原生”密封类。

@SealedOptions 注解支持一个 mode 注解属性,它可以取三个值之一(AUTO 是默认值)

NATIVE

生成与 Java 相似的类。在 JDK17 之前的 JDK 上编译时会产生错误。

EMULATE

表示类使用 @Sealed 注解进行密封。此机制适用于 JDK8+ 的 Groovy 编译器,但 Java 编译器不识别。

AUTO

为 JDK17+ 生成原生记录,否则模拟记录。

您是使用 sealed 关键字还是 @Sealed 注解与模式无关。