美文网首页
闭包(Closure)之 Kotlin vs Java

闭包(Closure)之 Kotlin vs Java

作者: 青叶小小 | 来源:发表于2021-01-01 12:32 被阅读0次

一、前言

闭包很早就有,最先接触的可能是前端同学(JavaScript)中对闭包的定义:

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包。

其中包括两个要点:

  • 函数
  • 周围环境(状态 & 上下文)

很多语言都把函数作为『一等公民』,Kotlin也不例外:

函数是“一等公民”的意思是,函数和变量一样,它是某种类型的实例,可以被赋值,可以被引用。

函数在Kotlin中主要包括三种形式:普通的具名函数匿名函数lambda表达式
周围环境可以理解为函数所处的外部作用域中定义的各个自由变量,这个作用域可能是另一个函数或块级作用域。
二者捆绑在一起构成的闭包使得函数内部可以对外部作用域定义的变量进行访问。

二、闭包(Java)

2.1、Java中不完整的闭包

protected void onCreate(Bundle savedInstanceState) {
    ...
    int num = 0;
    btn.setOnClickListener(v -> {
        num++;//报错
    });
}

上面的写法会被IDE提示:Variable used in lambda expression should be final or effectively final
因为根据Java规定:匿名内部类内部,方法或块级作用域内的具名内部类内部使用的外部变量必须是final的。

为什么会有这种规定呢?
因为对于Java闭包中的函数而言,周围环境(外部函数或块级作用域)在执行到结束时就会销毁,这里的周围环境对应与虚拟机栈中的一个栈帧。
而栈帧中的局部变量,在方法返回后就会被虚拟机回收。

正因为如此,Java中的闭包其实并不完整:
从闭包的定义来理解,Java函数中对周围环境中变量的引用无法阻止周围环境的销毁。从Java闭包的实现来看,这种对周围环境中变量的引用只是一种假象,编译器会对使用到的周围环境中的变量拷贝一份副本,而函数内部操作的只是不可见的副本而已。对这份不可见副本的修改不仅毫无意义,还会引起歧义,误以为周围环境中的变量真的被修改。
因此,Java干脆从语言层面禁止了这种修改,也就有了上述规定。

所以,Java中函数不是『一等公民』!

2.2、Java中闭包的变相实现

在 Java 中,匿名类就是代替闭包而存在的!只不过 Java 严格要求所有函数都需要在类里面,所以巧妙的把“声明一个函数”这样的行为变成了“声明一个接口”或“重写一个方法”。匿名类也可以捕获当前环境的 final 局部变量。但和闭包不一样的是,匿名类无法修改捕获的局部变量(final 不可修改)。

Java8 lambda 是进一步接近闭包的特性,lambda 的 JVM 实现是类似函数指针的东西。
Java7 的 lambda 语法糖兼容不是真正的 lambda,它只是简化了匿名类的书写。
同样的 lambda 也只能引用 final 变量。

2.3、绕过 Java 语法限制

这种因Java中不完整闭包导致的限制在实际开发中有诸多不便,因此常见到长度为1的数组这种技巧绕过限制:

protected void onCreate(Bundle savedInstanceState) {
    ...
    int[] num = new int[]{1};
    btn.setOnClickListener(v -> {
        num[0]++;//正确
    });
}

因为数组对象被分配到堆内存中,原来函数所需的,外部环境中的存在于栈帧中的变量到了堆内存中,会因函数内部引用而不被销毁。

实际上,Java中这种会被内部函数所修改的变量,更多时候我们会把它作为一个类的属性,而不是放在函数或块作用域的内部而受到栈帧出栈的影响:

int num = 0;
protected void onCreate(Bundle savedInstanceState) {
    ...
    btn.setOnClickListener(v -> {
        num++;//正确
    });
}

换个角度看,如果我们把闭包中周围环境(外部函数或块级作用域)的概念扩展一下,把类中定义的属性也做为函数所处的周围环境的话,那么类本身就可以认为是一个广义的闭包。

三、闭包(Kotlin)

对于Kotlin来说,就不存在 Java 的一些限制(需要声明外部变量为 final ),其实现是将函数使用到的周围环境中的变量,包装为一个新类中的属性!

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    var num = 0;
    btn.setOnClickListener {
        num++ //正确
    }
}

使用 jadx 反编译查看(等价于 Java 中的如下代码):

protected void onCreate(Bundle savedInstanceState) {
    ...
    final Ref.IntRef num = new Ref.IntRef();
    num.element = 0;
    btn.setOnClickListener(v -> {
        num.element++;
    });
}

函数中访问的外部环境中的变量,都会用Ref中定义的各种类型的包装类进行包装,对其的访问会进行自动的装箱拆箱工作。与长度为1的数组这种技巧一样,将外部环境中的变量引用的位置由栈移入的堆中。

如果我们在一个 kt 文件中编写如下代码:

fun returnFun(): () -> Int {
    var count = 0
    return { count++ }
}

fun main() {
    val function = returnFun()
    val function2 = returnFun()
    println(function())  // 0
    println(function()) // 1
    println(function()) // 2

    println(function2()) // 0
    println(function2()) // 1
    println(function2()) // 2
}

分析上面的代码,returnFun返回了一个函数,这个函数没有入参,返回值是Int。
我们可以用变量接收它,还可以调用它。function和function2分别是我创建的两个函数实例。

可以看到,我每调用一次function(),count都会加一,说明count 被function持有了而且可以被修改。
而function和function2的count是独立的,不是共享的。

用 jadx 反编译查看:

public final class ClosureKt {
    @NotNull
    public static final Function0<Integer> returnFun() {
        IntRef intRef = new IntRef();
        intRef.element = 0;
        return (Function0) new 1<>(intRef);
    }

    public static final void main() {
        Function0 function = returnFun();
        Function0 function2 = returnFun();
        System.out.println(((Number) function.invoke()).intValue());
        System.out.println(((Number) function.invoke()).intValue());
        System.out.println(((Number) function2.invoke()).intValue());
        System.out.println(((Number) function2.invoke()).intValue());
    }
}

被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。这个 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。

Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRef,FloatRef 等,如果是非基础类型,就统一用 ObjectRef 即可。

Ref 家族源码:https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/Ref.java

四、总结

通过以上我们可以总结如下:

  • 闭包不是新东西,是把函数作为“一等公民”的编程语言的特性;
  • 匿名类是 Java 世界里的闭包,但有局限性,即只能读 final 变量,不能写任何变量;
  • Kotlin 的闭包可以捕获上下文的局部变量,并修改它。实现办法是 Kotlin 编译器给引用的局部变量封装了一层引用。

相关文章

  • 闭包(Closure)之 Kotlin vs Java

    一、前言 闭包很早就有,最先接触的可能是前端同学(JavaScript)中对闭包的定义: 函数和对其周围状态(le...

  • JavaScript----闭包

    javascript之闭包 闭包的概念     闭包(closure)是 JavaScript 的一种语法特性。 ...

  • 关于rust中的闭包(一)

    闭包 在计算机中,闭包 Closure, 又称词法闭包 Lexical Closure 或函数闭包 functio...

  • 关于闭包

    闭包的英文是closure,又称词法闭包(Lexical Closure)和函数闭包(Function Closu...

  • 理解闭包

    闭包 何为闭包 闭包(Closure)是词法闭包(Lexical Closure)的缩写 高级程序设计中写有权访问...

  • python之闭包与装饰器

    1 闭包 维基百科给出的解析:闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭...

  • 闭包,定时器

    问题 1.什么是闭包? 有什么作用 闭包(英语:Closure),又称词法闭包(Lexical Closure)或...

  • [Code] 优雅地使用python闭包

    在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(funct...

  • golang:函数闭包

    From wiki 闭包在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure...

  • java,kotlin,dart闭包

    java闭包 kotlin闭包 dart闭包 了解不同语言之间的相同和不同。学习和进步。

网友评论

      本文标题:闭包(Closure)之 Kotlin vs Java

      本文链接:https://www.haomeiwen.com/subject/iieroktx.html