面向对象

本章涵盖 Groovy 编程语言的面向对象方面。

1. 类型

1.1. 原始类型

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

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

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

  • boolean 类型(truefalse 之一)

  • 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 类兼容。它们可以拥有与 Java 相同的修饰符(public、protected、private、static 等)的方法、字段和属性(可视为 JavaBeans 属性,但代码量更少)。源级别有一些细微差异,稍后将解释。

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(即使 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。在接口中使用 protectedprivate 方法是错误的。

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 接口那样的默认实现。如果您正在寻找类似(但不完全相同)的东西,特征与接口接近,但允许默认实现以及本手册中描述的其他重要功能。

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 实例化时提供了 nameage 参数

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

注意事项

  • 虽然上面的示例没有提供构造函数,但您也可以提供一个无参构造函数,或者一个第一个参数是 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 方法调用,带有额外的 Integer 类型的 number 参数
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: 没有适用于参数类型 (LinkedHashMap, Integer) 值 ([name:Marie, age:1], 23) 的 foo() 方法签名,因为命名参数 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. 可变参数 (Varargs)

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,而不是一个长度为一且唯一元素为 null 的数组。

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

如果使用数组作为参数调用可变参数方法,则参数将是该数组,而不是一个长度为一且唯一元素为给定数组的数组。

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

或许不出所料,使用 StringInteger 参数调用 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'

Object 数组优先于 Object。

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()
}

您也无需在 try/catch 块中将对上一个示例中 badRead 方法的调用括起来 - 尽管如果您愿意,可以这样做。

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

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

shouldFail(FileNotFoundException) {
    badRead()
}

3.3. 字段和属性

3.3.1. 字段

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

  • 一个强制性的访问修饰符publicprotectedprivate

  • 一个或多个可选的修饰符staticfinalsynchronized

  • 一个可选的类型

  • 一个强制性的名称

class Data {
    private int id                                  (1)
    protected String description                    (2)
    public static final boolean DEBUG = false       (3)
}
1 一个名为 idprivate 字段,类型为 int
2 一个名为 descriptionprotected 字段,类型为 String
3 一个名为 DEBUGpublic 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 遵循相同的约定,但提供了一种更简单的方式来定义属性。您可以使用以下方式定义属性:

  • 缺失访问修饰符(没有 publicprotectedprivate

  • 一个或多个可选的修饰符staticfinalsynchronized

  • 一个可选的类型

  • 一个强制性的名称

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

class Person {
    String name                             (1)
    int age                                 (2)
}
1 创建一个私有 String name 支持字段,一个 getName 和一个 setName 方法
2 创建一个私有 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'])

根据约定,即使没有提供支持字段,只要有遵循 JavaBeans 规范的 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'                      (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 将通过将属性名称首字母大写并添加 getset 前缀(或可选地为布尔型 getter 添加“is”)来形成。因此,getLength 将是 length 属性的 getter,setFirstName 将是 firstName 属性的 setter。isEmpty 可能是名为 empty 的属性的 getter 方法名称。

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

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

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

属性上的修饰符

我们已经看到通过省略可见性修饰符来定义属性。通常,任何其他修饰符(例如 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 确认属性访问时初始化
带有显式支持字段的分割属性定义

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

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

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

class HasPropertyWithProtectedField {
    protected String name  (1)
    String name            (2)
}
1 名称属性的受保护支持字段,而不是普通的私有字段
2 声明名称属性

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

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

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

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

如果类中存在 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 只允许应用于 TYPEMETHOD

可能的 target 列表可在 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 如果 valuestatusCode 都需要设置,则需要为默认的 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 如果方法是 public 且不带参数
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 提供给 @Explosiveafter 值参数会覆盖 @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 类型的值,但 @Bar 定义了 int 类型的值。

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

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
特征只支持 publicprivate 方法。不支持 protectedpackage private 作用域。

5.1.4. 最终方法 (Final methods)

如果我们有一个类实现了特征,从概念上讲,特征方法中的实现是“继承”到类中的。但是,实际上,没有包含此类实现的基础类。相反,它们直接织入类中。方法上的 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 类实现了 FlyingAbilitySpeakingAbility
2 创建 Duck 的新实例
3 我们可以调用 FlyingAbilityfly 方法
4 也可以调用 SpeakingAbilityspeak 方法

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

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 trait 定义了一个单独的 name 属性
2 Polite trait 继承 Named trait
3 Polite 添加了一个新方法,该方法可以访问超 trait 的 name 属性
4 name 属性在实现 PolitePerson 类中是可见的
5 introduce 方法也是如此

5.8.2. 多重继承

或者,一个 trait 可以继承多个 trait。在这种情况下,所有超 trait 都必须在 implements 子句中声明

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

5.9. 鸭子类型和 trait

5.9.1. 动态代码

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

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. Trait 中的动态方法

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

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 方法的 trait
2 Dynamic 类定义了一个属性
3 Dynamic 类定义了一个方法
4 调用现有属性将调用 Dynamic 中的方法
5 调用不存在的属性将调用 trait 中的方法
6 将调用 trait 上定义的 setProperty
7 将调用 trait 上定义的 getProperty
8 调用 Dynamic 上的现有方法
9 但通过 trait 的 methodMissing 调用一个不存在的方法

5.10. 多重继承冲突

5.10.1. 默认冲突解决

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

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

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

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

5.10.2. 用户冲突解决

如果这种行为不是你想要的,你可以使用 Trait.super.foo 语法明确选择要调用的方法。在上面的例子中,我们可以通过这样写来确保调用 trait A 中的方法

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

5.11. Trait 的运行时实现

5.11.1. 运行时实现 trait

Groovy 还支持在运行时动态实现 trait。它允许你使用 trait "装饰"现有对象。例如,让我们从这个 trait 和下面的类开始

trait Extra {
    String extra() { "I'm an extra method" }            (1)
}
class Something {                                       (2)
    String doSomething() { 'Something' }                (3)
}
1 Extra trait 定义了一个 extra 方法
2 Something实现 Extra trait
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 关键字在运行时将对象强制转换为 trait
2 然后可以在对象上调用 extra
3 并且 doSomething 仍然可以调用
当将一个对象强制转换为一个 trait 时,操作的结果不是同一个实例。可以保证被强制转换的对象将实现该 trait 以及原始对象实现的接口,但结果将不是原始类的实例。

5.11.2. 同时实现多个 trait

如果您需要同时实现多个 trait,可以使用 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
当将一个对象强制转换为多个 trait 时,操作的结果不是同一个实例。可以保证被强制转换的对象将实现这些 trait 以及原始对象实现的接口,但结果将不是原始类的实例。

5.12. 行为链

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

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

然后你可以通过应用小行为来组合一个消息处理器。例如,让我们以 trait 的形式定义一个默认处理器

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

然后任何类都可以通过实现 trait 来继承默认处理器的行为

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 trait 继续

这可行,但这种方法有缺点

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

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

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

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 使它将调用委托给链中的下一个 trait

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

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,这意味着链中的下一个 trait。在这里,下一个 trait 是 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. Trait 中 super 的语义

如果一个类实现多个 trait 并且发现了对不合格 super 的调用,则

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

  2. 如果链中没有剩余的 trait,super 指的是实现类的超类(this

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

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 的 trait,假设在运行时应用于 StringBuilder
2 重新定义 append 方法
3 从字符串中删除所有“o”
4 然后委托给 super
5 如果调用 toString,则委托给 super.toString
6 StringBuilder 实例上运行时实现 Filtering trait
7 附加的字符串不再包含字母 o

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

5.13. 高级特性

5.13.1. SAM 类型强制转换

如果一个 trait 定义了一个抽象方法,则它可以作为 SAM(Single Abstract Method)类型强制转换的候选。例如,想象以下 trait

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

由于 getNameGreeter trait 中的唯一抽象方法,您可以编写

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 中,接口可以有方法的默认实现。如果一个类实现了一个接口并且没有为默认方法提供实现,那么将选择接口中的实现。Trait 的行为相同,但有一个主要区别:如果类在其接口列表中声明了 trait 并且它没有提供实现,即使超类提供了实现,trait 中的实现也总是被使用。

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

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

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

在这个例子中,我们创建了一个简单的测试用例,它使用两个属性(configshell)并在多个测试方法中使用它们。现在想象一下,您想测试相同的东西,但使用另一个不同的编译器配置。一个选项是创建 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

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 代码时忘记更改的风险。即使 setup 已在超类中实现,但由于测试类在其接口列表中声明了 trait,因此行为将从 trait 实现中借用!

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

值得注意的是,如果您使用运行时 trait,trait 中的方法总是优先于代理对象的方法
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 是一个 trait,它定义 getName 返回 Bob
3 默认对象将返回 Alice
4 p2 在运行时将 p 强制转换为 Bob
5 getName 返回 Bob,因为 getName 取自 trait
再次强调,不要忘记动态 trait 强制转换会返回一个不同的对象,该对象只实现原始接口和 trait。

5.14. 与 Mixin 的区别

与 Groovy 中可用的 mixin 存在一些概念上的差异。请注意,我们谈论的是运行时 mixin,而不是已弃用并被 trait 取代的 @Mixin 注解。

首先,在 trait 中定义的方法在字节码中是可见的

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

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

  • 这些方法在 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 优于 trait 的一个地方:实例不会被修改,因此如果你将某个类混合到另一个类中,不会生成第三个类,并且响应 A 的方法即使被混合进去也会继续响应 A。

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

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

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

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

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

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

  • 通常不应混合具有相同签名的静态方法和实例方法。应用 trait 的正常规则适用(包括多重继承冲突解决)。如果选择的方法是静态的,但某些已实现的 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 静态字段在 trait 中声明
2 在 trait 中也声明了一个静态方法
3 静态字段在 trait 内部更新
4 一个静态方法 init 可用于实现类
5 静态字段被重新映射以避免钻石问题

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

Foo.CALLED = true

因为 trait 本身没有定义名为 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 实现 trait
2 Baz 也实现 trait
3 init 只在 Bar 上调用
4 Bar 上的静态字段 CALLED 已更新
5 Baz 上的静态字段 CALLED 未更新,因为它不同

5.16. 状态继承的陷阱

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

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

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

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

调用 f 的结果是 3,因为 f 委托给 trait 中的 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 从 trait 调用 sum

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

assert elem.f() == 3

原因是 sum 方法访问 trait 的字段。因此,它使用的是 trait 中定义的 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. 对 trait 的类型约束

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

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 为可以调用服务的设备定义一个通信 trait
4 MyDevice 定义为通信设备
5 调用 trait 中的方法,并解析 id

这里很清楚,Communicating trait 只能应用于 Device。然而,没有明确的契约来表明这一点,因为 trait 不能扩展类。然而,代码编译和运行都完美无缺,因为 trait 方法中的 id 将动态解析。问题是没有任何东西可以阻止 trait 应用于不是 Device 的任何类。任何具有 id 的类都可以工作,而任何没有 id 属性的类都将导致运行时错误。

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

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

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

如果您想在 trait 中能够调用 this,那么您需要显式地将 this 强制转换为 Device。这很快就会变得难以阅读,因为到处都是显式强制转换到 this

5.17.2. @SelfType 注解

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

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

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

因此在前面的例子中,我们可以使用 @groovy.transform.SelfType 注解修复 trait

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

现在,如果您尝试在不是设备的类上实现此 trait,将发生编译时错误

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

错误将是

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

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

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

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

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 trait 的用法都必须实现或扩展 HasHeightHasArea
2 只有 UnitCubeUnitCylinder 可以使用 trait

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

final class Foo implements FooTrait {}

然后,或者

@SelfType(Foo)
trait FooTrait {}

或者

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

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

5.18. 限制

5.18.1. 与 AST 转换的兼容性

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

5.18.2. 前缀和后缀操作

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

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

一个解决方法是改用 += 运算符。

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 方法来定制记录的 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,因为它不会因这个不可变记录而改变。我们还忽略了 null 值(我们定义中 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 方法来完成,该方法接受命名参数。记录组件从提供的参数中设置。对于未提及的组件,将使用原始记录组件的(浅)复制。以下是您可能如何使用 copyWithFruit 记录的示例

@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. 密封层次结构(孵化中)

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

  • 将类设为 final 以禁止扩展。

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

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

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

密封层次结构在已知类、接口和 trait 层次结构内提供完整的继承,但禁用或只提供层次结构外部的受控继承。

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

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)]'

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

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/@NonSealed。我们预计样式检查工具 CodeNarc 最终将有一个规则来查找 non-sealed 的存在,因此希望采用更严格样式的开发人员将能够使用 CodeNarc 和该规则。

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

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

@SealedOptions 注解(@Sealed 的一部分)支持一个 mode 注解属性,它可以取以下三个值之一(AUTO 为默认值)

NATIVE

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

EMULATE

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

AUTO

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

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