面向对象

本章涵盖 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

Character

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 会将您的运算符编译成其 方法等价物,并使用它们代替。此外,Groovy 会在调用接受基本类型参数的 Java 方法时自动拆箱为基本类型,并自动装箱来自 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 对应物之间的主要区别在于

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

  • 没有可见性修饰符的字段会自动转换为属性,这会导致代码更简洁,因为不需要显式 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。在接口中使用 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 会用对无参数构造函数的调用替换命名构造函数调用,然后对每个提供的命名属性调用设置器。

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

  • 您可以通过提供位置构造函数以及无参数构造函数或 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,而不是长度为一的数组,其中 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()
}

您也不必在前面的示例中的 badRead 方法调用周围添加 try/catch 块 - 尽管如果您愿意,可以这样做。

如果您希望声明代码可能抛出的任何异常(已检查或其他),您可以随意这样做。添加异常不会改变其他 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 私有字段 idIDGenerator.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 创建一个后备 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 也将识别属性,只要存在遵循 JavaBeans 规范的 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 的名称将通过大写属性名称并添加 getset 前缀(或可选地对布尔 getter 添加 "is")来形成。因此,getLength 将是 length 属性的 getter,而 setFirstName 将是 firstName 属性的 setter。isEmpty 可能是名为 empty 的属性的 getter 方法名。

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

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

由于 JavaBeans 规范中特殊的“缩略词处理”属性命名逻辑,属性名称的转换是非对称的。这会导致一些奇怪的边缘情况。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 确认在属性访问时进行初始化
使用显式支持字段的拆分属性定义

当你的类设计遵循某些与常见 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 声明带注释的 name 属性,用于 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 一个定义了类型为 Stringvalue 成员的注释
2 一个定义了类型为 Stringvalue 成员,其默认值为 something 的注释
3 一个定义了类型为基本类型 intstep 成员的注释
4 一个定义了类型为 ClassappliesTo 成员的注释
5 一个定义了类型为另一个注释类型的数组的 value 成员的注释
6 一个定义了类型为枚举类型 DayOfWeekdayOfWeek 成员的注释

与 Java 语言不同,在 Groovy 中,注释可以用来改变语言的语义。对于 AST 变换来说尤其如此,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 中找到。

Groovy 不支持在 Java 8 中引入的 java.lang.annotation.ElementType#TYPE_PARAMETERjava.lang.annotation.ElementType#TYPE_PARAMETER 元素类型。

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 保留

可能的保留 target 列表及其描述可以在 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 支持 *预编译* 和 *源代码形式* 的元注释。这意味着你的元注释 *可能* 被预编译,或者你可以在与你当前正在编译的代码相同的源代码树中拥有它。

INFO: 元注释是 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注解定义了类型为Stringvalue成员
2 @Bar注解也定义了类型为Stringvalue成员
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参数,该参数可用于更改默认处理器在存在重复注解时如何处理注解替换。

INFO:自定义处理器(将在下文中讨论)可能支持也可能不支持此参数。

例如,假设您创建一个包含@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. 特质

特质是语言的一种结构性构造,它允许

  • 行为的组合

  • 接口的运行时实现

  • 行为覆盖

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

它们可以看作是同时具有**默认实现**和**状态**的**接口**。特质使用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 修饰符只是指示编织方法的修饰符是什么。虽然继承和覆盖或多重继承具有相同签名但具有最终和非最终变体组合的方法可能被认为是不好的风格,但 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 我们可以调用 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 name 属性从实现 PolitePerson 类可见。
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 Filtering特性在StringBuilder实例上的运行时实现
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() { /* ... */ }
}

在此示例中,我们创建了一个简单的测试用例,它使用两个属性(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 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. 与 mixins 的差异

与 Groovy 中可用的 mixins 有几个概念上的差异。请注意,我们正在谈论运行时 mixins,而不是@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 mixin 到 A 中
4 我们可以调用methodFromA
5 我们还可以调用methodFromB
6 该对象是A的实例
7 但它不是B的实例

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

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

以下说明应谨慎对待。静态成员支持正在开发中,并且仍在实验阶段。以下信息仅适用于 4.0.12。

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

  • 具有静态方法的特性无法静态编译或类型检查。所有静态方法、属性和字段都是动态访问的(这是 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,该特性具有状态。但如果我们改为这样做呢?

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('[email protected]', '[email protected]', 'Hello!')
assert msg.toString() == 'Message[[email protected], [email protected], 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 值来实现,因为它不会改变这个不可变的记录。我们还忽略了 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 方法来完成,该方法接受命名参数。记录组件从提供的参数中设置。对于没有提到的组件,使用原始记录组件的(浅层)副本。以下是使用 Fruit 记录使用 copyWith 的方法

@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 和 non-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/@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 注解,都与模式无关。