23. Groovy 面向对象编程-Traits特性学习-第四篇 高级功能

2023-02-23 17:38:14 浏览数 (2)

1. 介绍

groovy学习笔记第23篇,接着学习关于traits的相关知识。

上一篇介绍了有关super关键字的影响。这篇开始介绍traits中的高级功能。

例如SAM类型强制,使用闭包模式实现只有一个抽象方法的traits对象创建。

例如方法继承与Java8的区别。与Mixins的差异。以及静态方法属性和字段等相关知识点,

2. 高级功能

介绍traits中的高级功能。

2.1 SAM类型强制

SAM (Single Abstract Method)翻译过来就是:单一抽象方法。如果traits定义了单个抽象方法,则它是SAM(单一抽象方法)类型强制的候选。示例如下:

代码语言:javascript复制
trait Greeter {
    String greet() { "Hello $name" }        
    abstract String getName()             //创建了一个抽象方法  
}

由于getName是Greeter特性中的唯一抽象方法,因此可以缩写为:

代码语言:javascript复制
Greeter greeter = { 'zinyan' }   //使用闭包模式创建
println(greeter.greet())  //输出:Hello zinyan

甚至可以修改为:

代码语言:javascript复制
void demo(Greeter g) { println g.greet() } 
demo { 'zinyan.com' }  //输出 Hello zinyan.com 

上面的示例进行一个简单介绍。首先创建了一个demo的方法,传入的对象是Greeter对象。

然后第二行就是调用demo方法,由于Greeter需要实习抽象方法,但是它只有一个String返回值的抽象方法。那么我们就可以通过{}创建一个闭包对象,来实现。

2.2 与Java 8默认方法的区别

在Java8中,接口可以具有方法的默认实现。如果一个类实现了一个接口,并且没有为默认方法提供实现,那么将选择该接口的实现。traits的行为相同,但有一个主要区别:如果类在其接口列表中声明了特性,并且即使超级类声明了特性也不提供实现,则始终使用特性的实现。

如果想覆盖已经实现的方法的行为,可以使用此功能以非常精确的方式组合行为。

具体示例如下:

代码语言:javascript复制
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
//创建一个类继承GroovyTestCase
class SomeTest extends GroovyTestCase {
    def config  //定义一个变量 config,数据类型不确定
    def shell //定义一个变量shell,数据类型不确定

    void setup() {
       //变量初始化
        config = new CompilerConfiguration()
        shell = new GroovyShell(config)
    }
    void testSomething() {
       println(shell.evaluate('1 1'))
        
    }
    void otherTest() { /* ... */ }
}

在本例中,我们创建了一个简单的测试用例,它使用两个属性(configshell),并在多个测试方法中使用这些属性。现在想象一下,如果想要测试相同的,但使用另一种不同的编译器配置。一个选项是创建SomeTest的子类:

代码语言:javascript复制
class AnotherTest extends SomeTest {
    void setup() {
        config = new CompilerConfiguration()
        config.addCompilationCustomizers( ... )
        shell = new GroovyShell(config)
    }
}

但如果我们实际上有多个测试类,并且想要测试所有这些测试类的新配置,该怎么办?我们必须为每个测试类创建一个不同的子类,例如:

代码语言:javascript复制
class YetAnotherTest extends SomeTest {
    void setup() {
        config = new CompilerConfiguration()
        config.addCompilationCustomizers( ... )
        shell = new GroovyShell(config)
    }
}

可以看到,这种测试类几乎是一样的。基于这种情况。我们可以通过traits来实现:

代码语言:javascript复制
trait MyTestSupport {
    void setup() {
        config = new CompilerConfiguration()
        config.addCompilationCustomizers( new ASTTransformationCustomizer(CompileStatic) )
        shell = new GroovyShell(config)
    }
}

然后在子类中使用:

代码语言:javascript复制
class AnotherTest extends SomeTest implements MyTestSupport {}
class YetAnotherTest extends SomeTest2 implements MyTestSupport {}
...

这将使我们能够大大减少样板代码,并减少在我们决定更改设置代码时忘记更改设置代码的风险。

即使设置已经在超级类中实现,由于测试类在其接口列表中声明了trait,所以行为也将从trait实现中借用!

当我们无法访问超级类源代码时,此功能特别有用。它可以用于模拟方法或强制子类中方法的特定实现。它允许我们重构代码以将重写的逻辑保持在一个特性中,并通过实现它来继承新的行为。当然,另一种方法是在使用新代码的每个地方重写该方法。

PS:需要注意的是,如果使用运行时traits,则traits中的方法总是优先于代理对象的方法:

示例如下:

代码语言:javascript复制
class Person {
    String name                                         
}
trait Zinyan {
    String getName() { 'zinyan' }                          
}

def p = new Person(name: 'Z同学')
    
println(p.name)      //输出 : Z同学  

def p2 = p as Zinyan      //强制转换为trait   
println(p2.name) //输出:zinyan                               

可以看到,运行时转为特性traits。特性中的方法将会优先,所以上面的示例中就会覆盖掉Person类本身的getName方法

3. 与Mixins的差异

混合元素在概念上有一些不同,因为它们在Groovy中可用。请注意,我们讨论的是运行时Mixin,而不是@Mixin注释,该注释不赞成使用traits

首先,traits中定义的方法在字节码中可见:

  • 在内部,特性表示为一个接口(没有默认或静态方法)和几个助手类。
  • 实现特性的对象有效地实现了接口
  • 这些方法在Java中可见
  • 它们与类型检查和静态编译兼容

相反,通过mixin添加的方法仅在运行时可见。示例如下:

代码语言:javascript复制
class A { String methodFromA() { 'A' } }        
class B { String methodFromB() { 'B' } }        
A.metaClass.mixin B          //通过mixin将B混合成A         
def o = new A()
println(o.methodFromA()) //输出 :A
println(o.methodFromB()) //输出:B

println(o instanceof A)  //输出: true
println(o instanceof B)  //输出:false                 

最后一点实际上是非常重要的,它说明了一个地方,即混合元素比特征更有优势:实例不会被修改,因此如果你将某个类混合到另一个类中,就不会生成第三个类,而响应a的方法将继续响应a,即使混合在一起。

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

静态构件当前是属于实验阶段。下面的内容仅适用于Groovy SDK 4.0.6版本

可以在traits中定义静态方法,但它有许多限制:

  • 具有静态方法的特性不能静态编译或类型检查。所有静态方法、属性和字段都是动态访问的(这是JVM的限制)。
  • 静态方法不会出现在为每个特性生成的接口中。
  • 特性被解释为实现类的模板,这意味着每个实现类都将获得自己的静态方法、属性和字段。因此,在trait上声明的静态成员不属于trait,而是属于它的实现类。
  • 通常不应混合使用相同名称的静态方法和实例方法。应用特性的常规规则适用(包括多重继承冲突解决,可以参考traits学习第二篇中的内容)。如果选择的方法是静态的,但某些实现的特性具有实例变量,则会发生编译错误。如果选择的方法是实例变量,则静态变量将被忽略(在这种情况下,行为类似于Java接口中的静态方法)。

示例如下所示:

代码语言:javascript复制
trait TestHelper {
    public static boolean CALLED = false     //在trait中 创建一个静态属性    
    static void init() {       //创建一个静态方法                 
        CALLED = true                           
    }
}
class Foo implements TestHelper {}
Foo.init()       //继承对象,可以直接使用静态方法,就和java中一样。静态方法可以直接使用而不用new一个对象后再调用
println(Foo.TestHelper__CALLED)     //输出 true                           

PS: 如果不明白为什么可以通过__访问变量,可以通过 https://zinyan.com/?p=446#5.2-public-变量访问 学习了解。

同时,不建议直接使用静态对象:

代码语言:javascript复制
Foo.CALLED = true

直接使用上面的静态对象并进行赋值操作,将会输出下面错误:

代码语言:javascript复制
Caught: groovy.lang.MissingPropertyException: No such property: CALLED for class: Foo
groovy.lang.MissingPropertyException: No such property: CALLED for class: Foo
	at zinyan.run(zinyan.groovy:10)

因为trait本身上没有定义静态字段CALLED。同样,如果您有两个不同的实现类,每个类都会得到一个不同的静态字段:

代码语言:javascript复制
trait TestHelper {
    public static boolean CALLED = false        
    static void init() {                        
        CALLED = true                           
    }
}
class Zin implements TestHelper {}              
class Yan implements TestHelper {}              
Zin.init()       
println(Zin.TestHelper__CALLED)  //输出:true
println(Yan.TestHelper__CALLED)  //输出: false

可以看到,这两个静态对象是不一样的。

5. 数据继承权

我们已经看到,traits是有状态的。traits可以定义字段或属性,但当类实现traits时,它会基于每个traits获取这些字段/属性。

示例:

代码语言:javascript复制
trait IntCouple {
    int x = 1
    int y = 2
    int sum() { x y }
}

我们创建一个Class来实现这个trait对象。

代码语言:javascript复制
class BaseElem implements IntCouple {
    int f() { sum() }
}
def base = new BaseElem()
println(base.f()) //输出:3

我们如果在Class中实现x和y这两个变量:

代码语言:javascript复制
class Elem implements IntCouple {
    int x = 3                                       
    int y = 4                                       
    int f() { sum() }                               
}
def elem = new Elem()
println(elem.f()) //输出:3

结果还是输出3。

原因是sum方法访问traits中的字段。所以它使用了traits中定义的x和y值。如果要使用实现类中的值,则需要使用getter和setter来取消引用字段。

代码实现效果如下所示:

代码语言:javascript复制
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()
println(elem.f()) //输出:7

6. 小结

本篇内容,介绍了SAM类型和Java 8中的一些区别特性。以及数据集成的逻辑等知识点。

相关内容可以参考Groovy官方文档:http://docs.groovy-lang.org/docs/groovy-4.0.6/html/documentation/#_chaining_behavior

下一篇,应该是traits知识的最后一篇内容了。如果觉得总结的合适,希望能够给我点个赞谢谢。

0 人点赞