上一节我们聊到Kotlin集合,以及它提供给我们的一些集合的扩展方法,我们主要探讨了filter跟map方法的实现,开销以及优化。那除了集合之外,Kotlin提供的lambda跟closure也很强大,今天我们来看看它们的使用对性能有什么影响。
首先来看一个lambda的例子:
fun main(args: Array<String>) {
val write = {
val ints = listOf(1, 2, 3, 4, 5)
File("somefile.txt")
.writeText(ints.joinToString("\n"))
}
invokeBlock(write)
invokeBlock(write)
}
fun invokeBlock(body: () -> Unit) {
try {
body()
} catch (e: Exception) {
e.printStackTrace()
}
}
write是一个lambda,我们知道在Kotlin里函数虽然是一级公民,但是编译器在编译时会把它是现成一个Function对象。我们来看看反编译成Java代码什么样子:
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Function0 write = (Function0)null.INSTANCE;
this.invokeBlock(write);
this.invokeBlock(write); }
两次调用方法居然只生成了一个对象,神奇不神奇?于是我深入到字节码里面看到:
// access flags 0x19
public final static LMain$main$write$1; INSTANCE
// access flags 0x8
static <clinit>()V
NEW Main$main$write$1
DUP
INVOKESPECIAL Main$main$write$1.<init> ()V
PUTSTATIC Main$main$write$1.INSTANCE : LMain$main$write$1;
RETURN
MAXSTACK = 2
MAXLOCALS = 0
在字节码里每个实现了Function接口的类都有一个静态字段保存了实例,提供了实例复用的能力,也就是我们所说的单例!一切编译器都给我们做好了。
再看一个例子:
fun test() {
val literals = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
var total = 0
literals.forEach {
total += it
} }
又是熟悉的集合操作,但是写完之后我总觉得哪里不对劲,依稀记得当初学Java8的lambda的时候,这么写编译器报错了困扰了好久,但是时间太久我不敢确定,于是赶紧用java写了一遍:
Error:(19, 49) java: local variables referenced from a lambda expression must be final or effectively final
果然,大脑给我的反馈还是对的,那为什么都是lambda行为却差这么多呢?
我们知道在Java里lambda跟匿名内部类在某种意义上是等价的,而Java里他们可以可以传递给其他线程,这就导致了Java如果允许lambda操作本地变量,可能会有一个并发问题,所以编译器不允许我们对局部变量进行修改。那Kotlin是怎么做到的呢?
我们看一下上面的方法反编译成Java代码:
public final void test() {
List literals = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9});
int total = 0;
Iterable $receiver$iv = (Iterable)literals; int it;
for(Iterator var4 = $receiver$iv.iterator(); var4.hasNext(); total += it) {
Object element$iv = var4.next();
it = ((Number)element$iv).intValue();
int var7 = false;
}
}
原来,编译器给优化成了循环。编译器还真是为了方便我们默默干了不少事啊!
好了,不扯题外话了,我们回到主题,除了lambda之外,Kotlin还有closure的概念,closure呢,其实就是一种访问了定义在函数外部的变量的函数,我们上面的lambda就可以说是一种closure。再确切一点:
val inc = { counter ++ }
我们这就定义了一个closure,我们可以把它当方法调用:
fun main(args: Array<String>) {
var counter = 0
val inc = {
counter ++
}
inc()
}
这里跟上面描述的情况有些类似,操作了外部定义的局部变量,但是有少许不同,在这里closure是被当成方法调用的,让我们来看看反编译后的Java代码:
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
final IntRef counter = new IntRef();
counter.element = 0;
Function0 inc = (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method public Object invoke() {
return this.invoke();
}
public final int invoke() {
IntRef var10000 = counter;
int var1;
var10000.element = (var1 = var10000.element) + 1;
return var1;
}
});
inc.invoke(); }
出现了一个很有意思的东西IntRef
,这里声明了一个final的IntRef对象,然后这个对象持有了实际会被修改的变量,我猜这也是编译器为了防止并发调用方法所做的措施,这里就不继续深究了。
我们来看一下生成的字节码:
// access flags 0x0
<init>(Lkotlin/jvm/internal/Ref$IntRef;)V
ALOAD 0
ALOAD 1
PUTFIELD Main$main$inc$1.$counter : Lkotlin/jvm/internal/Ref$IntRef;
ALOAD 0
ICONST_0
INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)V
RETURN
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1010
final synthetic Lkotlin/jvm/internal/Ref$IntRef; $counter
这个类生成了一个构造器,需要传入一个IntRef对象,类里面有个final字段引用这个对象,这导致了我们每次使用这些修改外部变量的lambda时,总会创建新的对象,编译器不会给我们缓存创建的实例,这对性能有不小的影响,建议大家在平时写代码时尽量避免这样使用lambda。
好了,这次更多的是lambda跟closure使用时的一些特性跟注意事项,倒没有涉及多少优化的层面,下回,我们来聊聊Kotlin属性访问可能存在的优化点。
快来关注我吧!
网友评论