Groovy 本质初探及闭包特性原理总结

Posted 工匠若水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Groovy 本质初探及闭包特性原理总结相关的知识,希望对你有一定的参考价值。

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

Groovy 语言的本质

Groovy 是基于 JVM 的一种语言,也是 Java 的一种升级拓展语言。其具备脚本语言的特性,但又不算脚本,所以其运行有如下两种方式:

  • 基于源码直接运行,譬如 groovy Test.groovy。
  • 基于预编译模式运行,譬如先 groovyc Test.groovy 然后 java Test。

无论上面的哪种运行方式,其本质都是 JVM 字节码,可以理解成和 java 编译运行没啥区别,只是 groovy 有自己的编译器,通过 java 命令运行其预编译的字节码需要在 classpath 额外加上 groovy 自己特有的 GDK jar 包,这也和 java 的 JDK 类似。使用一张图可能更加直观,如下:

Groovy 的基础特性

  • Groovy 中方法调用的圆括号是可选的。
  • Groovy 的==操作符不象 Java 其本质是调用了对象的 equals 方法进行比较,而不是判断引用是否相等。
  • Groovy 中缺省的方法访问修饰符是 public。
  • GroovyBean 提供了一种比 JavaBean 更加简洁的属性访问机制。
  • Groovy 与 Java 互相使用是无缝的,其对象之间不存在桥梁,每个 Groovy 对象都是一个 Java 对象,运行时也指向同一个对象。
  • Groovy 的 GDK 库会对 Java 的 JDK 进行很多拓展;譬如 GDK 中的 size() 方法可以用于 String、Array、List、Map 及其他容器,其背后的本质都是 JDK 类的改进,所以 size() 方法在 Groovy 的 String 上面就等价于 length 操作。
  • Groovy 的任意值都是对象类型,即便譬如数字 1、2 这种数字也是,其本质都会被转为对象包装类型,所以可以直接对数字做.操作调用其 Integer 相关方法属性,特别注意1+2这种操作在 Java 会被先计算,而在 Groovy 中由于都是对象,所以会被转换为对象 1 调用其plus(对象 2)得到一个新对象。
  • Groovy 中一切事物都是对象,所有的操作符都是作为方法调用进行处理的,譬如+实际调用的是对象的plus(val)方法;我们也可以重写这些运算符,本质就是重写plus(val)方法,不像 Java 需要实现一个特定的接口。
  • Groovy 支持一般字符串和 GString,一般的字符串是java.lang.String的实例,GString 是groovy.lang.GString的实例,GString 允许有占位符并且允许在运行时对占位符进行解析和计算。
  • Groovy 单引号字符串不会按照 GString 的类型来转义$处理内容,其等价于 java 的字符串;双引号字符串内容中包含的$符号按照 GString 实例特性处理;三组引号(或者是多行字符串)允许字符串的内容在多行出现,新的行被转换为\\n,其他所有的空白字符都被完整的按照文本原样保留,本质也是一个 GString 实例。
  • Groovy 在闭包体外面任何出现 return 的地方都会导致离开当前方法,当在闭包体内出现 return 语句时,仅仅结束闭包的当前计算,不会结束包含闭包的方法。

Groovy 闭包本质

Groovy 中闭包是最有用的特性之一,也是非常重要的特性,可以说 Groovy 精髓都围绕闭包。

  • 一个闭包是被包装为一个对象实例的代码块,实际上闭包像一个可以接受参数并且能够有返回值的方法。
  • 闭包是一个普通对象,因为你能够通过一个变量引用到它,正如你能够引用到任何别的对象一样。
  • 基于 Groovy 语言本质介绍可以知道,JVM 根本就不知道你正在运行 Groovy 代码,所以 Groovy 闭包是一个普通对象是很自然的现象。
  • Groovy 在集合对象上增加了许多额外的方法(each、 find、findAll、collect 等),使用闭包来指定这些每次都被执行的代码块会变得容易且直观。

声明闭包的方式

在一个方法调用的后面或者变量等号右侧,放置闭包代码在一对花括号里,闭包的参数和代码通过箭头->进行分隔,一个参数的可以省略箭头,可以使用默认的it隐式参数。特别注意,花括号显示了闭包声明的时间,不是执行的时间。

简单声明方式的闭包

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

def testStr = ''
(1..10).each(
    testStr += it
)
assert testStr == '12345678910' //true

赋值方式声明闭包

def Closure getPrintCustomer() 
    return  line -> println line 


def printCustomer =  line -> println line 

可以看到,无论如上哪种声明闭包方式,花括号都是其中的主角,而在 Groovy 中的花括号作用一般都是用来标明构建了一个新的闭包对象或者一个 Groovy 代码块。

  • Groovy 代码块:可以是类、接口、static、对象的初始化代码、方法体,或者与 Groovy 关键字(if、else、synchronzied、for、while、switch、try、catch 和 finally)一起出现的花括号。
  • 一个新的闭包对象:除过 Groovy 代码块外的其它花括号的出现形式基本都是闭包。

同理,上面两种声明方式也能发现,闭包就是一个 Closure 对象实例,他们可以通过变量进行引用,能够作为参数传递,可以在闭包中调用方法,也可以作为一个方法的返回实例出现。

重用已有的方法声明闭包

一般我们的方法会有一个方法体,可选的返回值,能接受参数,并且能被调用。而通过上面两种闭包的声明和使用能发现,闭包的结构和特性和普通方法也很相似,因此 Groovy 可以让我们作为一个闭包重用已经在方法中存在的代码。

引用一个方法作为闭包是使用reference.&操作符reference是闭包调用时使用的对象实例,就像普通方法调用 reference.someMethod()一样。.&可以被称为方法闭包操作符。

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

class MethodClosureTest 
    int size;
    MethodClosureTest(int size) 
        this.size = size;
    

    boolean validSize(String str) 
        return str.length() >= size;
    

//使用
MethodClosureTest m1 = new MethodClosureTest(4);
Closure clo = m1.&validSize;    //方法闭包
def li = ['111', '2', '55555'];
li.find(clo);   //'55555'

可以看到,上面展示了传递闭包给方法执行的各种闭包声明方式,到此你至少能看懂闭包的声明和调用了,至于其闭包的原理我们下面接着看。

调用闭包

假设引用 closure 指向一个闭包,则可以通过closure.call([params])closure([params])来调用闭包。

def test =  x, y = 5 ->
    return x + y

//闭包调用
assert test(4, 3) == 7
assert test.call(4, 3) == 7

assert test(7) == 12
assert test.call(7) == 12

闭包语法的 class 代码

上面我们见证了闭包的声明和调用,为了循序渐进,我们这里先看看上面的代码的 class 字节码到底是啥样的,因为前面我们说过,Groovy 本质就是 JVM 语言,其编译后也是 class,只是 classPath 比 JDK 多了 GDK 而已。

//test.groovy
def testStr = ''
(1..10).each(
    testStr += it
)
print(testStr)

如上代码使用groovyc test.groovy编译产物为test.classtest$_run_closure1.class,_run_closure1 为 test 的内部类名,我们使用 IDEA 拖入这两个产物 class 文件查看反编译如下(这是反编译产物,实际可能有出入):

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

import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.lang.Reference;
import groovy.lang.Script;
import groovy.transform.Generated;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
//就是一个普通的 class 类,继承自 GDK 的 Script 类
public class test extends Script 
    //父类的重写,不是我们关注重点,忽略
    public test() 
        CallSite[] var1 = $getCallSiteArray();
        super();
    
    //父类的重写,不是我们关注重点,忽略
    public test(Binding context) 
        CallSite[] var2 = $getCallSiteArray();
        super(context);
    
    /**
     * 重点!!!
     * 入口代码
     */
    public static void main(String... args) 
        CallSite[] var1 = $getCallSiteArray();
        var1[0].call(InvokerHelper.class, test.class, args);
    

    public Object run() 
        CallSite[] var1 = $getCallSiteArray();
        //这就是我们定义的 testStr 接收字符串变量
        final Reference testStr = new Reference("");
        /**
         * 闭包被转换为继承 Closure 的普通 java 类
         */
        final class _run_closure1 extends Closure implements GeneratedClosure 
            public _run_closure1(Object _outerInstance, Object _thisObject) 
                CallSite[] var4 = $getCallSiteArray();
                super(_outerInstance, _thisObject);
            
            //闭包的调用本质调用的是这个方法,下面章节有源码解释
            public Object doCall(Object it) 
                CallSite[] var2 = $getCallSiteArray();
                Object var10000 = var2[0].call(testStr.get(), it);
                testStr.set(var10000);
                return var10000;
            

            public Object getTestStr() 
                CallSite[] var1 = $getCallSiteArray();
                return testStr.get();
            

            @Generated
            public Object doCall() 
                CallSite[] var1 = $getCallSiteArray();
                return this.doCall((Object)null);
            
        
        //range的翻译
        var1[1].call(ScriptBytecodeAdapter.createRange(1, 10, (boolean)1), new _run_closure1(this, this));
        return var1[2].callCurrent(this, testStr.get());
    

上面代码不用看懂所有,也没有必要,你只要领悟精髓即可,即 Groovy 的一切都是对象,闭包的本质就是 Closure 实例对象即可,其他都可以理解成是 Java 的一种拓展,你甚至可以理解成他就是 Java 的一个三方框架,只是这个框架不仅仅提供了代码编写的规范,还有自己的一套构建规则,这才是本质思想。

闭包的委托策略

渐渐的我们慢慢揭开了闭包的面纱,Groovy 的闭包有委托的概念,即在闭包中修改委托对象和委托策略的能力,能够让它在 DSL(Domain Specific Languages)领域(譬如 Gradle 等)得到升华。理解闭包的委托功能,就需要先弄明白groovy.lang.Closure类的三个属性,即 this、owner 和 delegate。

this、owner 和 delegate

this: 指向定义闭包所在的封闭类。在闭包中可以通过getThisObject()方法获取定义闭包的封闭类,和显式调用 this 是等价的效果。这个不用再举例子了,比较简单,可以看下一小节的案例。

owner: 对应定义声明闭包的封闭对象,可以是类或者闭包。它与 this 有点类似,最大的不同是 owner 指向的是直接包裹它的对象(普通对象或者闭包对象)。这个不用再举例子了,比较简单,可以看下一小节的案例。

delegate: 可以对应任何对象。this 和 owner 都属于闭包的标准语法范围,而 delegate 指向的是闭包所使用的用户自定义的对象。默认情况下 delegate 被设置成 owner。我们可以通过 delegate 关键字和getDelegate()setDelegate(x)方法来使用委托对象。样例如下:

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

class Animal 
    String name

class Cat 
    String name


def dog = new Animal(name: 'dog')
def cat = new Cat(name: 'catter')

def nameLength =  delegate.name.length() 
/**
 * 默认 delegate 等价于 owner,即声明闭包所属的对象,此处为 Script
 * 所以运行直接报错,因为 delegate 指向的对象没有 name 属性
 */
println(nameLength())
/**
 * 闭包 delegate 指向 dog 对象
 * 所以运行打印为 3
 */
nameLength.delegate = dog
println(nameLength())
/**
 * 闭包 delegate 指向 cat 对象
 * 所以运行打印为 6
 */
nameLength.delegate = cat
println(nameLength())

深入委托策略

在 Java 类的实例方法中调用类的方法和引用属性时,我们可以省略方法或属性前的this(譬如this.func()this.property可简写为func()property),表示调用或引用的是本实例的方法或属性。

这个特性在 Groovy 中同样适用,而且 Groovy 在 闭包 Closure 中调用方法和引用属性时,我们也可以省略方法和属性前的delegate(譬如delegate.func()delegate.property可简写为func()property),表示调用或引用的是本闭包的方法或属性(而闭包 Closure 通过 delegate 隐式变量将方法调用和变量引用委派给了 delegate 引用的那个对象)。

闭包 delegate 的默认值是 Closure 的隐式变量 owner,而 owner 通常对应定义声明闭包的封闭对象,可以是类或者闭包。因此,无论什么时候,在闭包中访问某个属性时如果没有明确地设置接收者对象,那么就会调用一个委托策略,譬如如下案例:

class ClosureTest 
    String str1 = "1"
    def outerClosure = 
        def str2 = "2"
        /**
         * 属性或者方法存在于 owner 内,那么他可以被 owner 调用
         * 所以 outerClosure 闭包的 owner 是其声明包裹的对象,即 test 实例
         * 故 str1 = 1
         */
        println str1
        def nestedClosure = 
            /**
             * 属性或者方法不存在于自己的 owner 内,而自己的 owner 又是一个闭包实例对象,所以在自己 owner 的 owner 上继续寻找调用
             * 故 str1 = 1
             */
            println str1
            /**
             * 如果属性或者方法存在于 owner 内,那么他可以被 owner 调用
             * 所以 nestedClosure 闭包的 owner 是其声明包裹的闭包对象,即 outerClosure 闭包实例
             * 故 str2 = 2
             */
            println str2
        
        nestedClosure()
    


ClosureTest test = new ClosureTest()
def closure = test.outerClosure
closure()
/**
 * 输出结果:
 * 1
 * 1
 * 2
 */

上面例子中没有显式的给 delegate 设置一个接收者,但是无论哪层闭包都能成功访问到 str1、str2 值,这是因为默认的解析委托策略在发挥作用,Groovy 闭包的委托策略有如下几种:

  • Closure.OWNER_FIRST:默认策略,首先从 owner 上寻找属性或方法,找不到则在 delegate 上寻找。
  • Closure.DELEGATE_FIRST:和上面相反,首先从 delegate 上寻找属性或者方法,找不到则在 owner 上寻找。
  • Closure.OWNER_ONLY:只在 owner 上寻找,delegate 被忽略。
  • Closure.DELEGATE_ONLY:和上面相反,只在 delegate 上寻找,owner 被忽略。
  • Closure.TO_SELF:高级选项,让开发者自定义策略,必须要自定义实现一个 Closure 类,一般我们这种玩家用不到。

下面是一些改变委托策略的样例,用来加深理解:

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

class Person 
    String name
    def pretty =  "My name is $name" 
    String toString() 
        pretty()
    

class Thing 
    String name


def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')

assert p.toString() == 'My name is Sarah'
p.pretty.delegate = t
/**
 * p 的 owner 上有 name 属性,默认策略 Closure.OWNER_FIRST,
 * 所以还是走的 p 的属性值。
 */
assert p.toString() == 'My name is Sarah'
/**
 * p 的 owner 上有 name 属性,策略是 Closure.DELEGATE_FIRST,
 * 所以和上面策略刚好相反,delegate 首先被委托,其次再委托给 owner,
 * 所以走的是 t 的属性值。
 */
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
class Person 
    String name
    int age
    def fetchAge =  age 

class Thing 
    String name


def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == 42
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42
cl.delegate = t
try 
    cl()
    assert false
 catch (MissingPropertyException ex) 
    // "age" is not defined on the delegate

如上就是闭包的委托策略,请无比记住,非常重要,因为 Gradle 这种基于 Groovy 的 DSL 语言的精髓之一就是这个特性,我们会在后面文章中对 DSL 做专门的分析。

闭包机制本质反思

搞懂了 Groovy 的委托策略,接下来就该循序渐进的来复盘下闭包机制的本质,所以需要先思考类似下面一个典型场景的代码原理。

def sum = 0
5.times  sum++ 
assert sum == 5

可以看到,传递给 times 方法的闭包可以访问变量 sum,在声明闭包的时候本地变量是可以访问的。那执行闭包期间闭包是怎么访问 sum 变量的呢?

首先,times 方法每次回调时是没机会知道变量 sum 的,所以 times 方法不可能直接传递 sum 给闭包,也不知道闭包使用了 sum 变量,因为前面一再强调了,闭包其实是一种解耦能力,调用方是不关心其实现的。那要想在运行时获得 sum 变量,唯一的可能就是闭包在它的运行时生命周期里以某种方式记住了它的工作上下文环境,当调用它的时候,闭包可以在它的原始上下文中工作。这种生命周期的上下文需要闭包记住相应的一个引用,而不是一个复制对象实例,如果工作上下文是原始上下文的一个复制对象实例,则从闭包中是没有办法改变原始上下文的。

如上这个结论的猜想其实就是闭包的委托策略,你可以从闭包语法的 class 代码闭包的委托策略两个小节就能悟到这个本质。

下面是一个典型案例(来自一本网络大佬的书籍实例):

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

class Mather 
    int field = 1
    int foo() 
        return 2
    
    /**
     * 指明这个方法将返回一个闭包对象
     * 注意:在声明期间闭包的列表将被返回,闭包仍然没有被调用
     */
    Closure birth(param) 
        def local = 3
        def closure =  caller ->
            [this, field, foo(), local, param, caller, this.owner]
        
        return closure
    


Mather julia = new Mather()
/**
 * 使用一个新的变量接收闭包
 * 注意:到现在 birth 返回的闭包还没有被调用,元素列表仍然还没有被构建
 */
closure = julia.birth(4)

/**
 * 显式调用 birth 返回的闭包
 * 闭包从它出生的地方构建元素列表,我们将列表保存在一个变量 context 方便使用
 * 注意我们把自身(本质是 Script 对象)作为参数传递给闭包,这样可以在闭包中作为 caller 使用
 */
context = closure.call(this)
/**
 * 输出了这次运行的时候的脚本类的名称
 */
println context[0].class.name
/**
 * foo() 的调用其实是 this.foo() 的缩写
 * 注意:this 指向引用到的闭包,而不是声明闭包的对象
 * 在这里闭包将所有的方法调用代理给 delegate 对象,delegate 缺省是声明闭包的对象(也就是 owner),这样保证了闭包在它的上下文中运行。
 */
assert context[1..4] == [1, 2, 3, 4]
assert context[5] instanceof Script
/**
 * 在闭包中,魔术变量 owner 引用到声明闭包的对象
 */
assert context[6] instanceof Mather
/**
 * 每次调用 birth 方法的时候,闭包都重新构建
 * 对于闭包来说,闭包的花括号就像 new 关键字一样
 * 这就是闭包和方法的根本差别,方法在类产生的时候就被构建为仅有的一次,闭包对象在运行的时候被构建,并且相同的代码也有可能被构建多次。
 */
firstClosure = julia.birth(4)
secondClosure = julia.birth(4)
assert firstClosure.is(secondClosure) == false

怎么样,到此 Groovy 闭包委托代理的精髓就介绍完毕了,下面就是该揭开闭包本质的时候了。

闭包 Closure 类源码

Groovy 闭包的本质是一个 Closure 实例对象,GDK 里的 groovy.lang.Closure 是一个普通的 java 类,其内部有很多特性,具体感兴趣的可以去看下源码,如下是对源码部分删减的简介:

public abstract class Closure<V> extends GroovyObjectSupport implements Cloneable, Runnable, GroovyCallable<V>, Serializable 
    /**
     * 熟悉的一堆闭包委托代理策略
     */
    public static final int OWNER_FIRST = 0;
    public static final int DELEGATE_FIRST = 1;
    public static final int OWNER_ONLY = 2;
    public static final int DELEGATE_ONLY = 3;
    public static final int TO_SELF = 4;

    public static final int DONE = 1, SKIP = 2;
    ......
    /**
     * 可见每个闭包对象实例内部都有一个委托对象属性
     */
    private Object delegate;
    /**
     * 可见每个闭包对象实例内部都有一个owner对象属性
     */
    private Object owner;
    /**
     * 可见每个闭包对象实例内部都有一个this对象属性
     */
    private Object thisObject;
    /**
     * 委托策略定义,默认是OWNER_FIRST
     */
    private int resolveStrategy = OWNER_FIRST;
    private int directive;
    /**
     * 闭包参数类型和个数相关属性
     */
    protected Class[] parameterTypes;
    protected int maximumNumberOfParameters;
    private static final long serialVersionUID = 4368710879820278874L;
    private BooleanClosureWrapper bcw;
    /**
     * 闭包构造方法,每个括号声明的闭包实例背后都是通过构造方法实例化的
     */
    public Closure(Object owner, Object thisObject) 
        this.owner = owner;
        this.delegate = owner;
        this.thisObject = thisObject;

        final CachedClosureClass cachedClass = (CachedClosureClass) ReflectionCache.getCachedClass(getClass());
        parameterTypes = cachedClass.getParameterTypes();
        maximumNumberOfParameters = cachedClass.getMaximumNumberOfParameters();
    
    ......
    /**
     * 熟悉的委托策略设置
     */
    public void setResolveStrategy(int resolveStrategy) 
        this.resolveStrategy = resolveStrategy;
    
    public int getResolveStrategy() 
        return resolveStrategy;
    
    /**
     * 熟悉的闭包内部 this 获取方法
     */
    public Object getThisObject()
        return thisObject;
    
    /**
     * 获取闭包属性,这就是一堆策略了,神马隐式简写本质都在这里体现了
     */
    public Object getProperty(final String property以上是关于Groovy 本质初探及闭包特性原理总结的主要内容,如果未能解决你的问题,请参考以下文章

Groovy闭包 Closure ( 闭包类 Closure 简介 | thisownerdelegate 成员赋值及源码分析 )

Groovy闭包 Closure ( 闭包定义 | 闭包类型 | 查看编译后的字节码文件中的闭包类型变量 )

Groovy闭包 Closure ( 闭包类 Closure 简介 | thisownerdelegate 成员区别 | 静态闭包变量 | 闭包中定义闭包 )

Gradle基础知识-Wrapper,Daeman;Groovy闭包语法

JS闭包的基础知识,闭包的本质,闭包的作用,闭包的间谍属性和闭包的遗憾

Groovy闭包 Closure ( 闭包调用 与 call 方法关联 | 接口中定义 call() 方法 | 类中定义 call() 方法 | 代码示例 )