Ref: 

本文从 JVM 字节码的层面,简易分析了 Kotlin 的 Lambda 表达式的基本原理。理解其底层的实现原理有利于写出更高效率的代码。

本文基本上全文参考了上述两篇博客,文中示例全部使用 Kotlin 1.4 进行了重新验证.

Lambda 表达式是 Java 8 提供的一项重大改变,其实现原理是依赖于编译器在编译阶段将 Lambda 表达式编译成匿名内部类,在这点上可以认为,Java 8 的 Lambda 表达式是本质为匿名内部类的语法糖.

Kotlin 自诞生起便天然地支持 Lambda 表达式,其背后的实现原理是否与 Java 相同呢?下面例举几个不同语法形式的 Kotlin Labmda 函数,通过反编译其字节码,来看看其背后的实现原理,以及存在的性能开销。

- Kotlin Lambda 表达式的性能问题



与 Java 8 Lambda 的实现原理类似,Kotlin 在 JVM 上对 Lambda 的支持也是通过编译器将 Lambda 表达式编译为内部类来实现的,可以使用 Android Studio 的 KotlinBytecode 工具 decompile 字节码进行查看. 其实可以认为,JVM 上的 Labmda 实现应该都会是同样的原理.

但是在使用 Kotlin Labmda 时,针对不同的语法,编译器还是会有不同的处理,这其中的差异会有一定的性能隐患:

1)

最简易的 Lambda 的表达式

new Thread{
print("hello")
}

编译后,实际上会生成一个实现 Runnable 接口的静态内部类,如下:

static class RunnableImp implement Runnable{
RunnableImp INSTANCE;

@Override public void run(){
print("hello);
}

static {
INSTANCE = new RunnableImp();
}
}

该静态类是单例的,我们今后每次执行该表达式,都会直接复用,并不会重新创建,因此具有性能优化的效果.

2)

Lambda 表达式中访问外部作用域的成员的情形

val msg = "hello"
new Thread{
print(msg)
}

不同于情形1,这种情形下,因为内部类访问了外部成员,因此每次都需要创建新的内部类,因此并不会有跟情形1一样的优化效果.

3)

// Kotlin object expression

new Thread(object : Runnable {
override fun run() {
println("hello world")
}
})

其对应的反编译Java代码为

new Thread((Runnable)new Runnable {
override public void run() {
println("hello world")
}
})

不习惯 Lambda 的同学,可能还是习惯与使用 Object expression 这样类似于 Java 语法匿名内部类的写法,但是实际上,这种使用方式是存在性能损耗的,因为它并不会使用如 情形1 那种形式的复用内部类的性能优化手段,而是每次都重新创建,因此性能上肯定不如情形1,因此在语法层面,如非必要,则尽量避免使用 object expression 形式的语法.

4)

Kotlin Lambda 中可以修改其访问到的外部成员



Java 的匿名内部类和 Lambda 表达式,若有访问到外部成员变量,则要求该变量必须为 final,其中原因此处不再赘述。因此,在内部类中或者 Lambda 中若要修改该外部变量是不可能的,真有这样的需求,通常的做法也是创建新的引用来对其进行操作.

// java

public void sayHi(){
final String msg = "hello";
new Thread(()->{
// 编译失败
msg += "world";
System.out.println(msg);
});
}

不同于 Java ,Kotlin "似乎"移除了这样的限制,我们在 Lambda 中竟然可以修改外部作用域的变量.

// kotlin

fun sayHi() {
var msg = "hello"
Thread {
// 通过编译
msg += "world"
println(msg)
}
}

其实不然,反编译上述代码的字节码为 Java 代码如下:

static class RunnableImp implement Runnable{
private final ObjectRef $msg;

public RunnableImp(ObjectRef $msg){
this.$msg = $msg;
}

@Override public void run(){
String var1 = Intrinsics.stringPlus((String)this.$msg.element, " world");
boolean var2 = false;
System.out.println(var1);
}
}

可以其使用了一个 ObjectRef 持有了外部的引用,故而可以对实现对该变量的操作. 与我们通常使用 Java 时的处理方式本质是一样的,只是 Kotlin 在编译层面避免了我们书写更多的模板代码.

ObjectRef 是 Kotlin SDK 中的类,源码如下:

public static final class ObjectRef<T> implements Serializable {
public T element;

@Override
public String toString() {
return String.valueOf(element);
}
}

总结:
1)Kotlin Lambda 实现本质依旧是在编译阶段将 Lambda 转换了等价的内部类实现;
2)Lambda 未访问作用域外部的变量时,每次执行都会使用同一个内部类;而若 Lambda 访问了作用域外部的变量,每次执行 Lambda 表达式,实质上都需要重新创建一个内部类;在这点上,Kotlin 其实进行了一定的性能优化.
3) 使用 object expression 可以实现与 Java 匿名内部类的同样效果,但是 object expression 每次都会创建新的内部类,因此若在语法层面可以使用 Labmda 代替 object expression,那么则应该尽量使用 Lambda 表达式.
3) Kotlin Lambda 中若访问外部成员,并不要求外部成员“不可变”,并且可以在其内部修改该变量.

todo:

  • 啥是 Lambda
  • Java Lambda 原理
  • Android 编译器 desugar
  • Kotlin Lambda 原理及性能
  • Android Kotlin 编译.
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐