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() { /* ... */ }
}
在本例中,我们创建了一个简单的测试用例,它使用两个属性(config
和shell
),并在多个测试方法中使用这些属性。现在想象一下,如果想要测试相同的,但使用另一种不同的编译器配置。一个选项是创建SomeTest的子类:
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知识的最后一篇内容了。如果觉得总结的合适,希望能够给我点个赞谢谢。