Groovy 动态面向对象复盘总结

1,784 阅读15分钟

工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~

背景

前面《Groovy 本质初探及闭包特性原理总结》文章中我们总结了 Groovy 的本质,可以发现 Groovy 不但增强了 java 的脚本能力,也提供了新的面向对象特性;就像前面看到的,Groovy 对 java 非对象基础类型直接变成了引用类型,引入了闭包,并为对象集合增加了许多简化符号和拓展能力;前面文章也说了,这些特性可以看作是 java 的一种语法糖,但如果 Groovy 的对象仅仅提供 java 的语法糖能力,那他就没现在这么火了,它之所以能火,本质还是因为它具备了一种 java 语法糖不具备的动态特性,关于这个特性我们下面会由浅入深的揭开面纱。

定义类和脚本

Groovy 中类的定义几乎和 java 一样,而脚本的定义是不同的。

属性和本地变量

Groovy 的本地变量作用范围被限制在所在的方法体内,属性是与类或这些类的实例相关联。类的属性和本地变量在使用之前必须进行声明。

Groovy 脚本允许使用没有声明的变量,在这种情况下变量被假定从脚本的 binding 属性获取,如果在 binding 中没有发现相应的变量,那么把变量增加到 binding 中,binding 是一个数据存储器,它能把变量在脚本调用者和脚本之间进行传递,现在先记住这个特性就行(其实你在写 gradle 时也会用到它,譬如你会发现 gradle 脚本中 def 定义的属性在 def 定义的方法中是无法访问的,解决方案之一就是用 script 的 binding 来存储)。

Groovy 对类属性也使用 java 的修饰符来修饰,只是属性的缺省修饰符与 java 不同,在没有指定范围修饰符的属性声明时,Groovy 会根据需要生成相应的访问方法(getter 和 setter 方法),这是 groovyBeans 的特性。

Groovy 中定义变量或者属性的类型是可选的,当没有类型和修饰符时,必须使用 def 来作为替换,实际上用 def 来表明属性或者变量是没有类型的(其内部本质将被声明为 Object 类型)。

Groovy 中除过可以通过obj.fieldName来引用属性之外,也可以通过下标操作符来引用属性,譬如:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

class Num {
    public counter = 0
}

def num = new Num()
num.counter = 1
assert 1 == num.counter
//动态执行
def fieldName = 'counter'
num[fieldName] = 2
assert num['counter'] == 2

上面其实就是 Groovy 动态执行的一种场景演示。

方法和参数

方法的声明也能用 java 的那些修饰符,方法的返回类型声明是可选的,如果没有修饰符或者返回类型,那么使用 def 关键字来替代,当使用 def 关键字的时候,方法的返回类型被认为是没有类型,缺省的方法访问范围是 public。

显式声明参数的类型是可选的,当声明类型被忽略的时候,Groovy 使用 Object 作为类型,显式声明参数类型和忽略参数类型是可以混合在一起使用的。

Groovy 支持使用 map 作为一个命名参数列表来声明参数,map 的 key 在方法中被用作参数使用,这和有些语言的原生命名参数支持是不一样的,因为你要时刻记得 Groovy 是一种 JVM 语言,它再怎么编译都得符合 JVM 规范,而 JVM 不支持把参数的名称保存在字节码中,所以只能曲线救国实现。

Groovy 中当调用对象引用上的一个方法时,我们一般是obj.methodName(),这种方式就和 java 一样限制了方法名的格式不能包含特殊字符(譬如-``.等),但是 Groovy 对其有了另一种强大的支持,即obj.’my.method-Name’()是可以调用成功的,这就使得实例的方法调用可以变成动态的,譬如obj."${var}"(),我们对 var 变量进行动态修改即可。

Groovy 提供了?.操作符来进行安全的引用,当操作符之前是一个 null 引用的时候,则表达式被终止且返回 null,这和 Kotlin 的?.是一个道理。

构造方法

Groovy 对 java 的构造方法进行了拓展,具体表现如下。

位置参数:构造方法默认是 public 的,我们可以通过三种途径调用构造方法,即常用的 java 方式,使用 as 关键字进行强制造型和使用隐式造型。

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

class MetaCreater {
    String name, address;
    //位置参数构造方法定义
    MetaCreater(name, address) {
        this.name = name;
        this.address = address;
    }
}

//当 groovy 看到需要将一个列表造型成别的类型的时候,
//它试图使用列表的所有元素按照顺序位置作为参数来调用该类型的构造方法,
//这在使用 as 关键字进行造型或者通过赋值来进行静态类型引用的时候是很有用的。
def a = new MetaCreater('aa', 'a-a');
def b = ['b', 'b-b'] as MetaCreater;
MetaCreater c = ['c', 'c-c']

命名参数:上面使用位置参数太多的时候就不灵活了,因为我们必须考虑到构造方法需要的可选参数场景,而命名参数就能很好的解决这个问题。

class MetaCreater {
    String name, address;
}

//隐式的命名构造方法不再有效
new MetaCreater()
//命名参数
new MetaCreater(name: 'a');
new MetaCreater(name: 'a', address: 'a-a');
new MetaCreater(address: 'a-a');

隐式构造方法:通过简单的提供构造方法的参数列表来隐式的调用构造方法。隐式构造方法经常用来作为 builder。

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

class MetaCreater {
    String name, address;
    //位置参数构造方法定义
    MetaCreater(name, address) {
        this.name = name;
        this.address = address;
    }
}

//隐式构造
MetaCreater c = ['c', 'c-c']

组织类和脚本

Groovy 类在字节码级别就是 Java 类,所以 Groovy 对象在内存中就是 Java 对象,在源代码级别 Groovy 类和对象处理几乎是 Java 语法的一个超集,只有嵌套类是一个例外,当前 Groovy 语法还不支持嵌套类,并且在数组的定义上有微小的改变。

文件和类的关系

  • 如果一个 groovy 文件不包括类声明,那么它被作为一个脚本处理。即包装成继承 Script 类实现,自动生成的类的名称与源文件名称相同,文件内容被包装进一个 run 方法中,并且增加了一个 main 方法用来启动脚本。
  • 如果 groovy 文件中只包含一个类声明,并且这个类的名称与文件名相同,则含义与 java 中一样。
  • 如果一个 groovy 文件包含多个不同访问范围的类(类名没必要和文件名一样)声明,groovyc 编译器完美的为所有在这个文件中声明的类创建*.class文件,如果你希望直接调用你的脚本,例如在命令行或者 IDE 中使用 groovy, 那么在你的文件中的第一个类中应该有一个 main 方法。
  • 如果一个 groovy 文件混合了类的声明和脚本代码,在这种情况下,脚本代码将变成一个主类被执行,因此不要声明一个与源文件同名的类。
  • 当没有进行显式编译的时候,groovy 通过匹配相应名称的*.groovy源文件来查找类,在这点上,名称变得十分重要,groovy 仅仅根据类名称是否匹配源文件名称来查找类,当这样的一个文件被找到的时候,在这个文件中声明的所有类都将被转换,并且 groovy 在后面的时间都将能找到这些类。

在包中组织类

Groovy 延续了 Java 包结构组织文件的方式,包结构用来在文件系统中找到相应的类文件。由于*.groovy源文件不需要编译成*.class文件,因此在查找类的时候也需要查找*.groovy文件,Groovy 查找*.groovy时重用了类路径。当我们查找一个给定的类的时候,如果 groovy 同时找到了一个*.class*.groovy文件,它使用最后修改的那个文件,也就是说,如果源文件在上一次编译之后做了修改,groovy 将重新编译源文件到*.class文件。

Groovy 跟随 Java 的导入语句,在声明类之前进行导入。默认情况下 groovy 导入了 6 个包和两个类,这使得每个 groovy 源代码程序看起来都包含了下面的初始化语句:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

import java.lang.*
import java.util.*
import java.io.*
import java.net.*
import groovy.lang.*
import groovy.util.*
import java.math.BigInteger
import java.math.BigDecimal

在 Groovy 中的 import 语句有另外一个强大的作用,它可以与 as 关键字一起使用,这可以用来作为类型的别名,而一般的 import 语句运行使用类的名称来应用来,通过类型别名你可以使用任何你喜欢的名称来引用一个类,这个特性解决了类名称冲突的问题并且支持本地修改一个第三方类库的特性。

//------------ 支持本地修改一个第三方类库的特性案例 ------------
package thirdparty

class MathLib {
    Integer twice(Integer value) {
        return value * 3 // intentionally wrong!
    }

    Integer half(Integer value) {
        return value / 2
    }
}

assert 10 == new MathLib().twice(5) //false
assert 2 == new MathLib().half(4) //true

//我们可以使用类型别名来重命名原始的类,然后使用继承来修复它,不用修改原来已经使用的代码。
import thirdparty.MathLib as OriginalMathLib

class MathLib extends OriginalMathLib {
    Integer twice(Integer value) {
        return value * 2 // fix third bug
    }
}

assert 10 == new MathLib().twice(5) //true
assert 2 == new MathLib().half(4) //true

//----------------- 类名称冲突的问题案例 ------------------
//假设我们需要使用下面的额外的数学库
package thirdparty2

class MathLib {
    Integer increment(Integer value) {
        return value + 1
    }
}

//上面的类虽然它在不同的包中,但是有一个与前面的类型相同的名称,
//如果不使用别名,那么我们在代码中必须有一个类使用全限定名称或者两个都使用全限定名称,
//通过别名,我们可以通过一种优雅的方式来避免这种情况。
import thirdparty.MathLib as T1MathLib
import thirdparty2.MathLib as T2MathLib

assert 3 == new T1MathLib().half(new T2MathLib().increment(5)) //true

高级 OO 特性

Groovy 除过具备 Java OO 的特性外,自己还有一些额外的特性。

Multimethods

Groovy 的方法查找是方法参数的动态类型化,而 java 是静态类型的,这个 groovy 的特性叫做复合方法 Multimethods。

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

def func(Object obj) {
    return 'object'
}

def func(String str) {
    return 'string'
}

Object x = 1
Object y = 'foo'

assert 'object' == func(x)
assert 'string' == func(y)  // groovy 中 true,java 中 func(y) 返回 object

参数 x 是 Object 静态类型和 Integer 动态类型,参数 y 是 Object 静态类型和 String 动态类型。两个参数有相同的静态类型,两次都采用与 java 等价的方式传递给 func(Object),由于 groovy 通过动态类型进行方法分派,所以 func(String) 的专门实现被用在第二中情况。

可以看到,调用 func 方法的地方可能完全不知道其差异,从一个调用者的角度看,它就像 func(String) 覆盖了 func(Object),这在 java 中是办不到的。所以在 groovy 中通过这种能力,能使我们通过适当的覆盖行为更好的避免重复的代码。

GroovyBean 特性

Groovy 通过特定的语言支持使得 JavaBean 的使用更加简单,这种使用包含了三个方面:

  • 创建 JavaBean 类特殊的 Groovy 语法;
  • 不管 JavaBean 是在 groovy 中还是在 java 中声明的,groovy 提供了容易访问 Bean 的机制;
  • 对 JavaBean 的事件处理提供支持;

一般我们都是通过.进行属性访问,但是也有一些特例,我们还可以通过.@进行属性访问,譬如:

class Bean {
    public value

    void setValue(value) {
        this.value = value
    }

    void getValue() {
        value * 2
    }
}

def bean = new Bean(value: 100)
assert 200 == bean.value
assert 100 == bean.@value

上面例子其实就是 Groovy 的一个特例规则,**在类内部,引用fieldName或者this.fieldName将被解释为直接属性访问,而不是 bean 风格的属性访问(通过访问方法进行);在类的外部,可以使用reference.@fieldName语法直接访问类属性,而不是 bean 风格的属性访问(通过访问方法进行)。

Groovy 不识别 bean 和其他类型的对象的区别,它仅仅依赖相应的 getter 和 setter 方法是否可用。我们可以通过一个实例对象的 getProperties 方法和 properties 属性来获取到实例对象属性的 map (key 为属性名称,value 为属性值)列表,操作样例如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

class SomeClass {
    def someProperty
    public someField
    private somePrivateField
}

def obj = new SomeClass()
def store = []
obj.properties.each { property ->
    store += property.key
    store += property.value
}

assert store.contains('someProperty')
assert store.contains('someField') == false
assert store.contains('somePrivateField') == false
assert store.contains('class')
assert store.contains('metaClass')
assert obj.properties.size() == 3

Groovy 支持通过赋值方式扩展属性,这个特性很像 javascript,具体如下:

def ex = new Expando()
assert null == ex.address

ex.address = 'abc'
assert 'abc' == ex.address

ex.callback = {count -> return this.address * count}
assert 'abcabc' == ex.callback(2)

Groovy MOP 元程序编程

Groovy 元程序编程是指在运行时改变对象和类行为的系统能力。这些东西其实只用知道大概即可,目前可以不用深究。

元类 MetaClass 概念

Groovy 所有的对象都实现了 GroovyObject 接口,它就像我们提到过的其它类一样,声明在groovy.lang包中,GroovyObject 看起来有如下的格式:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

public interface GroovyObject {
    public Object invokeMethod(String name, Object args);
    public Object getProperty(String property);
    public void setProperty(String property, Object newValue);
    public MetaClass getMetaClass();
    public void setMetaClass(MetaClass metaClass);
}

在 groovy 中你的所有类都是通过 GroovyClassGenerator 来构建的,因此它们都实现了 GroovyObject 这个接口。如果你希望一个一般的 java 类也被作为一个 Groovy 类来组织,你必须实现 GroovyObject 接口,为了便利性,你可以扩展抽象类 GroovyObjectSupport 类,其提供了默认的实现。

GroovyObject 与 MetaClass 协作,MetaClass 是 Groovy 元概念的核心,它提供了一个 Groovy 类的所有的元数据,如可用的方法、属性列表,MetaClass 常用的接口方法如下:

Object invokeMethod(Object obj, String methodName, Object args)
Object invokeMethod(Object obj, String methodName, Object[] args)
Object invokeStaticMethod(Object obj, String methodName, Object[] args)
Object invokeConstructor(Object[] args)
......

上面这些方法进行真正的方法调用工作,使用 JAVA 反射 API 或者通过透明创建一个反射类,GroovyObject 的 invokeMethod 方法默认实现总是转到相应的 MetaClass 中,而 MetaClass 被存储在一个名称为 MetaClassRegistry 的中心存储器中,同时 groovy 也从 MetaClassRegistry 中获取 MetaClass。所以当 Groovy 处理方法调用的时候请想到下面这个图:

在这里插入图片描述

特别注意:MetaClassRegistry 类被设计为单例模式,不能被直接实现,在代码中可以使用 InvokerHelper 的一个工厂方法来引用到这个单例的注册中心。GroovyObject 引用到的 MetaClass 是 GroovyObject 在 MetaClassRegistry 中注册的类型,他是不需要相同,例如,一个特定的对象可以有一个特殊的 MetaClass(这个 MetaClass 可以与该对象类的其他对象的 MetaClass 不一样)。

方法调用和拦截

Groovy 生成 java 字节码,所以其每一个方法的处理都遵循下列机制之一:

  1. 类自己实现 invokeMethod 的方法(也许被代理到 MetaClass);
  2. 它自己的 MetaClass 通过getMetaClass().invokeMethod()进行调用;
  3. 在 MetaClassRegistry 中注册的该类型的 MetaClass;

Groovy 每个方法调用时都有自己复杂的决策流程,很多时候我们其实是不需要考虑这些的,只是说明白这些细节是有价值的,这样在复杂的情况下你总是可以正确的工作,它也为你在你的类中增加动态行为提供了各种可能。包括了下面的可能性:

  • 你能够使用 aspects 进行方法调用的拦截,如日志跟踪、应用安全限制,强制事务控制等。
  • 你能够中转调用别的方法,例如一个包装类能中转所有的方法调用到被包装的对象上(其实闭包就是这样做的,它将方法调用代理给它的 delegate)。
  • 可以假装执行一个方法,这样可以应用一些特殊逻辑,例如,一个 Html 类可以假装有一个方法 body,当调用 body 方法的时候执行print(‘body’)

Groovy 方法调用逻辑有多种方式来实现拦截、中转或者伪装方法:

  • 实现/覆盖在 GroovyObject 中的 invokeMethod 方法来伪装或者中转方法调用(意味着覆盖点方法操作符);
  • 实现/覆盖在 GroovyObject 中的 invokeMethod 方法,并且也实现 GroovyInterceptable 接口来增加方法的拦截调用代码;
  • 提供一个 MetaClass 的实现类,并且在目标 GroovyObject 对象上调用 setMetaClass;
  • 提供一个 MetaClass 的实现类,并且在 MetaClassRegistry 中为所有的目标类(Groovy 和 java 类)进行注册,这种方案通过 ProxyMetaClass 进行支持;

由此就解释了 Groovy 动态语言的精髓在此。

【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 blog.csdn.net/yanbober