浅析一下:kotlin委托背后的实现机制
大家好,kotlin的属性委托、类委托、lazy等委托在日常的开发中,给我们提供了很大的帮助,我之前的文章也是有实战过几种委托。不过对比委托实现的背后机制一直都没有分析过,所以本篇文章主要是带领大家分析下委托的实现原理,加深对kotlin的理解。
一. lazy
委托
这里我们不说用法,直接说背后的实现原理。
先看一段代码:
val content: String by lazy {
"oiuytrewq"
}
fun main() {
println(content.length)
}
我们看下反编译后的java代码:
- 首先会通过
DelegateDemoKt
静态代码块饿汉式的方式创建一个Lazy
类型的变量content$delegate
,命名的规则即代码中定义的原始变量值拼接上$delegate
,我们原始定义的content变量就会从属性定义上消失,但会生成对应的get方法,即getContent()
;
- 当我们在
main
方法中调用content.length
时,其实就是调用getContent().length()
,而getContent()
最终是调用了content$delegate.getValue
方法;
- 这个lazy类型的变量是调用了
LazyKt.lazy()
方法创建,而真正的核心逻辑——该方法具体参数的传入,在反编译的java代码中并没有体现;
java代码既然看不到,我们退一步看下字节码:
上面是DelegateDemoKt
类构造器对应的字节码,其中就是获取了DelegateDemoKt$content$2
作为参数传入了LazyKt.lazy()
方法。
我们看下DelegateDemoKt$content$2
类的实现字节码:
DelegateDemoKt$content$2
类实现了Function0
接口,所以上面lazy
的真正实现逻辑就是DelegateDemoKt$content$2
类的invoke
方法中,上图的字节码红框圈出的地方就很直观的看出来了。
二. 属性委托
属性委托的委托类就是指实现了ReadWriteProperty
和ReadOnlyProperty
接口的类,像官方提供的Delegates.observable()
和Delegates.vetoable()
这两个api也是借助前面两个接口实现的。这里我们就以支持读写的ReadWriteProperty
委托接口进行举例分析。
先看一段例子代码:
var age: Int by object : ReadWriteProperty<Any?, Int> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return 10
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
val v = value * value
println("setValue: $v")
}
}
fun main() {
age = 4
println(age)
}
我们看下反编译的java代码:
public final class DelegateDemoKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.mutableProperty0(new MutablePropertyReference0Impl(DelegateDemoKt.class, "age", "getAge()I", 1))};
@NotNull
private static final <undefinedtype> age$delegate = new ReadWriteProperty() {
@NotNull
public Integer getValue(@Nullable Object thisRef, @NotNull KProperty property) {
Intrinsics.checkNotNullParameter(property, "property");
return 10;
}
public void setValue(@Nullable Object thisRef, @NotNull KProperty property, int value) {
Intrinsics.checkNotNullParameter(property, "property");
int v = value * value;
String var5 = "setValue: " + v;
System.out.println(var5);
}
};
public static final int getAge() {
return age$delegate.getValue((Object)null, $$delegatedProperties[0]);
}
public static final void setAge(int var0) {
age$delegate.setValue((Object)null, $$delegatedProperties[0], var0);
}
public static final void main() {
setAge(4);
int var0 = getAge();
System.out.println(var0);
}
}
- 和lazy有些类似,会生成一个实现了
ReadWriteProperty
接口的匿名类变量age$delegate
,命名规则和lazy相同,通过还帮助我们生成了对应的getAge
和setAge
方法;
- 当我们在代码中执行
age = 4
就会调用setAge(4)
方法,最终会调用age$delegate.setValue()
方法;类似的调用age
就会调用getAge()
,最终调用到age$delegate.getValue()
方法;
- 编译器还通过反射帮助我们生成了一个
KProperty
类型的$$delegatedProperties
变量,主要是ReadWriteProperty
的setValue
和getValue
方法都需要传入这样一个类型的对象,通过$$delegatedProperties
变量我们可以访问到具体的变量名等信息;
类似的还有一种属性委托,我们看下代码:
val map = mutableMapOf<String, Int>()
val name: Int by map
上面代码的意思是:当访问name时,就会从map这个散列表中获取key为"name"的value值并返回,不存在就直接抛异常,接下来我们看下反编译后的java代码:
public final class DelegateDemoKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.property0(new PropertyReference0Impl(DelegateDemoKt.class, "name", "getName()I", 1))};
@NotNull
private static final Map map = (Map)(new LinkedHashMap());
@NotNull
private static final Map name$delegate;
static {
name$delegate = map;
}
public static final int getName() {
Map var0 = name$delegate;
Object var1 = null;
KProperty var2 = $$delegatedProperties[0];
return ((Number)MapsKt.getOrImplicitDefaultNullable(var0, var2.getName())).intValue();
}
}
- 生成一个Map类型的
name$delegate
变量,这个变量其实就是我们定义的map
散列表;
- 通过反射生成了一个
KProperty
类型对象变量$$delegatedProperties
,通过这个对象的getName()
我们就能拿到变量名称,比如这里的"name"变量名;
- 最终调用了
MapsKt.getOrImplicitDefaultNullable
方法,去map
散列表去查找"name"这个key对应的value;
PS:记得kotlin1.6还是1.7的插件版本对应委托进行了优化,这个后续的文章会再进行讲解。
三. 类委托
类委托实现就比较简单了,这里我们看下样例代码:
fun interface Fruit {
fun type(): Int
}
class FruitProxy(private val model: Fruit) : Fruit by model
fun main() {
val proxy: FruitProxy = FruitProxy {
-1
}
println(proxy.type())
}
反编译成java代码看下:
首先我们看下FruitProxy
这个类,其实现了Fruit
接口,借助属性委托特性,编译器会自动帮助我们生成type()
接口方法的实现,并再其中调用构造方法传入的委托类对象model
的type()
方法,类委托的核心逻辑就这些。
再main()方法中构造FruitProxy
时,我们也无法知晓具体的构造参数对象是啥,和上面的lazy一样,我们看下字节码:
其实FruitProxy
方法就传入了一个DelegateDemoKt$main$proxy$1
类型的对象,并实现了Fruit
接口重写了type
方法。
总结
本篇文章主要是讲解了三种委托背后的实现原理,有时候反编译字节码看不出来原理的,可以从字节码中寻找答案,希望本篇文章能对你有所帮助。
历史文章
这里是我整理的过往kotlin特性介绍的历史文章,大家感兴趣可以阅读下:
kotlin密封sealed class/interface的迭代之旅
优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!
@JvmDefaultWithCompatibility优化小技巧