1,属性委托
by lazy在kotlin中使用是很常见的,用于实现懒加载某个数据。而这两个单词不是一体的,其中by是kotlin中的关键字,用于实现委托;lazy是一个方法,他的返回值是委托的具体对象。
因此,想要了解by lazy的实现,则必须先去明白属性委托的机制。委托就是将本身的实现交给别的对象去实现,因此,若要实现属性委托,则需要将属性的get/set方法交给委托对象去实现。
/**
* thisRef 属性所属的对象,可为null
* property 属性相关的一些参数,比如属性名称等
*/
operator fun getValue(thisRef: Any?, property: KProperty<*>)
/**
* thisRef 属性所属的对象,可为null
* property 属性相关的一些参数,比如属性名称等
* value 设置的值,因为本例中委托的是String,所以value的类型也写成了String
*/
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String)
上面是属性的get/set方法,当类实现了这两个方法后,他就能作为属性委托的对象。其中val属性的委托对象只用实现getValue方法即可,var属性则额外多一个setValue方法。
/**
* 定义一个用于属性委托的对象
*/
class DelegateDemo {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "getValue: 所属对象:$thisRef, 参数名称:${property.name}"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("setValue: 所属对象:$thisRef, 参数名称:${property.name}, 设置的值:$value")
}
}
fun main() {
var demoString by DelegateDemo()
// 使用demoString,这时候会去调用DelegateDemo的getValue方法
println(demoString)
// 设置demoString的值,这时候去会调用setValue方法
demoString = "main"
}
/**
* 输出结果:
* getValue: 所属对象:null, 参数名称:demoString
* setValue: 所属对象:null, 参数名称:demoString, 设置的值:main
*/
因为demoString直接定义在了主函数中,所以所属对象为null。我们将kotlin反编译成java代码去看看:
public final class BKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty0(new MutablePropertyReference0Impl(BKt.class, "demoString", "<v#0>", 1))};
public static final void main() {
DelegateDemo var10000 = new DelegateDemo();
KProperty var1 = $$delegatedProperties[0];
DelegateDemo demoString = var10000;
String var2 = demoString.getValue((Object)null, var1);
boolean var3 = false;
System.out.println(var2);
demoString.setValue((Object)null, var1, "main");
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
可以看到,在main函数中实例化了一个DelegateDemo对象,当我们使用demoString的时候实际上是用的是DelegateDemo#getValue方法,而设置值的时候也是调用的Delegate#setValue方法。
2,by lazy
通过by委托不只是能够进行属性委托,同样也能够对对象进行委托,但是这里主要为了介绍by lazy ,所以就不再赘述对象委托了。
通过前面对属性委托的简单介绍,我们也明白了属性委托的机制。by后面跟着的肯定是一个对象,也就是委托对象,该对象负责属性的get/set,所以lazy这个方法最终肯定会返回一个对象。
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)
lazy方法一共有三个,我们最常用的是第一个。从代码中可以看出,lazy方法最终是创建的Lazy<T>的实例,这个实例也就是属性委托的对象。
public interface Lazy<out T> {
public val value: T
public fun isInitialized(): Boolean
}
Lazy一共有三个子类,其中我们使用的第一个方法返回的是SynchronizedLazyImpl,这是Lazy的其中一个实现。
internal object UNINITIALIZED_VALUE
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
// 委托的属性的值由_value记录,初始值是单例对象UNINITIALIZED_VALUE
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
// 已初始化则直接返回对应值
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
// 未初始化的,执行传递进来的lambda参数进行赋值
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
private fun writeReplace(): Any = InitializedLazyImpl(value)
}
SynchronizedLazyImpl是Lazy的一个子类,可以看到在value属性的get方法中,会去判断是否已经初始化过,若是没有初始化,则会调用initializer去进行初始化,赋值给_value。若是已经初始化过,则会直接返回 _value的值。因此by lazy就能通过这种方式去实现懒加载,并且只加载一次。
从代码中可以看出,SynchronizedLazyImpl是保证了线程安全的。是通过DCL方式来保证的安全,能够确保在多线程下也只会执行一次代码块。
但是我们说过,要实现可读属性委托,必须实现getValue方法。而在SynchronizedLazyImpl中,并没有实现getValue方法,而是只有value属性的get方法。这里猜测编译器应该是对Lazy有特殊的处理,而通过实验,实现了Lazy接口的对象确实可以作为只读属性的委托对象。而其他接口即使与Lazy一模一样,实现它的对象也不能作为属性委托对象。下面可以再写个demo验证这一点:
class Demo {
val demoString by lazy { "aa" }
}
// 反编译成java
public final class Demo {
@NotNull
private final Lazy demoString$delegate;
@NotNull
public final String getDemoString() {
Lazy var1 = this.demoString$delegate;
Object var3 = null;
boolean var4 = false;
return (String)var1.getValue();
}
public Demo() {
this.demoString$delegate = LazyKt.lazy((Function0)null.INSTANCE);
}
}
从上面可以看到,创建了一个demoStringdalegate#getValue()方法。
这里分析的是SynchronizedLazyImpl,实际上Lazy的另外的两个子类也是差不多的逻辑,这里不再多说。
3,总结
by lazy是通过属性代理来实现的懒加载,只在第一次使用的时候才会去执行表达式,并且只会执行一次。
by lazy默认是线程安全的,内部通过双重判断锁来保证只执行一次代码块赋值
当能够确定不会发生在多线程中的时候,可通过lazy(LazyThreadSafetyMode.NONE) { ... }来避免加锁。
4,附1
在上面的Demo中,我们看到demoString$delegate是在构造方法中去赋值的,通过LazyKt.lazy方法去创建,但是为什么lazy方法参数是null.INSTANCE?这里是有一些问题的,实际上参数应该是我们传入的lambda表达式{ "aa" },我们可以通过字节码去查看:
$ javap -c Demo.class
Compiled from "Demo.kt"
public final class com.example.hiltdemo.other.Demo {
public final java.lang.String getDemoString();
Code:
0: aload_0
1: getfield #11 // Field demoString$delegate:Lkotlin/Lazy;
4: astore_1
5: aload_0
6: astore_2
7: aconst_null
8: astore_3
9: iconst_0
10: istore 4
12: aload_1
13: invokeinterface #17, 1 // InterfaceMethod kotlin/Lazy.getValue:()Ljava/lang/Object;
18: checkcast #19 // class java/lang/String
21: areturn
public com.example.hiltdemo.other.Demo();
Code:
0: aload_0
1: invokespecial #25 // Method java/lang/Object."<init>":()V
4: aload_0
5: getstatic #31 // Field com/example/hiltdemo/other/Demo$demoString$2.INSTANCE:Lcom/example/hiltdemo/other/Demo$demoString$2;
8: checkcast #33 // class kotlin/jvm/functions/Function0
11: invokestatic #39 // Method kotlin/LazyKt.lazy:(Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
14: putfield #11 // Field demoString$delegate:Lkotlin/Lazy;
17: return
}
Demo.class中有两个方法,其一是getDemoString(),他的实现与上面的java代码是一致的,那么可以看看另一个方法也就是构造方法有什么不同:
public com.example.hiltdemo.other.Demo();
Code:
0: aload_0
1: invokespecial #25 // Method java/lang/Object."<init>":()V
4: aload_0
5: getstatic #31 // Field com/example/hiltdemo/other/Demo$demoString$2.INSTANCE:Lcom/example/hiltdemo/other/Demo$demoString$2;
8: checkcast #33 // class kotlin/jvm/functions/Function0
11: invokestatic #39 // Method kotlin/LazyKt.lazy:(Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
14: putfield #11 // Field demoString$delegate:Lkotlin/Lazy;
17: return
前两行是调用父类初始化方法也就是super()方法,直接从标号5看起,获取到静态参数com/example/hiltdemo/other/Demo2.INSTANCE,
然后是标号11,调用kotlin/LazyKt.lazy方法,将前面的INSTANCE作为参数,然后标号14将结果赋值给demoString$delegate。
也就是说,构造方法中传入的不是null.INSTANCE,而是Demo2.INSTANCE。按照前面说的,Demo2.INSTANCE就是传入的表达式了,实际上也确实如此:
$ javap -c 'Demo$demoString$2.class'
Compiled from "Demo.kt"
final class com.example.hiltdemo.other.Demo$demoString$2 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<java.lang.String> {
public static final com.example.hiltdemo.other.Demo$demoString$2 INSTANCE;
public java.lang.Object invoke();
Code:
0: aload_0
1: invokevirtual #12 // Method invoke:()Ljava/lang/String;
4: areturn
public final java.lang.String invoke();
Code:
0: ldc #15 // String aa
2: areturn
com.example.hiltdemo.other.Demo$demoString$2();
Code:
0: aload_0
1: iconst_0
2: invokespecial #22 // Method kotlin/jvm/internal/Lambda."<init>":(I)V
5: return
static {};
Code:
0: new #2 // class com/example/hiltdemo/other/Demo$demoString$2
3: dup
4: invokespecial #41 // Method "<init>":()V
7: putstatic #43 // Field INSTANCE:Lcom/example/hiltdemo/other/Demo$demoString$2;
10: return
}
可以看到,Demo2继承了kotlin.jvm.internal.Lambda并且实现了kotlin.jvm.functions.Function0<java.lang.String>接口,其实lambda表达式最终都会被封装成具体的对象来使用。而INSTANCE是静态常量,在static块中赋值的,所以lambda表达式封装的对象都是通过静态常量INSTANCE来实现的单例以供使用。
其中Function0是个interface,定义在kotlin.jvm.functions中,其中一共有23个接口,从Function0到Function22,他们的区别就是参数的个数不同,因为我们by lazy的表达式中没有参数,所以实现的是Function0接口。具体的实现都封装在invoke方法中。
5,附2
前面有说过,lambda表达是最终会被封装成具体的对象,因为前面的表达式是{ "aa" },是无参数的表达式,因此Demo2实现的是Function0接口。
public interface Function0<out R> : Function<R> {
public operator fun invoke(): R
}
再看看Demo2:
$ javap -p 'Demo$demoString$2.class'
Compiled from "Demo.kt"
final class com.example.hiltdemo.other.Demo$demoString$2 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<java.lang.String> {
public static final com.example.hiltdemo.other.Demo$demoString$2 INSTANCE;
public java.lang.Object invoke();
public final java.lang.String invoke();
com.example.hiltdemo.other.Demo$demoString$2();
static {};
}
在Demo2中,有两个invoke方法。Object invoke()和String invoke(),为什么会有两个呢,为什么又能有两个呢?这跟我们说的重载是不一样的,重载是指参数列表不同,而这两个方法显然是不满足的。
这是因为在class字节码层面,一个方法的描述是包括返回值和方法名和参数列表的。但是在java语言层面,方法描述是不包含返回值的,因此,在代码中我们是不能这样写的。但是由于我们实现了Function<String>接口,所以会有一个String invoke()方法。另外由于泛型擦除,该方法实际上是Object invoke(),而实现一个接口必须实现他的方法,所以在class中就出现了两个invoke方法。
Object invoke()属于桥接方法,本身的实现就是直接调用对应的方法,本例中是直接调用String invoke(),这点从上面的字节码中也可以看出。该方法外部无法直接调用,但是可以通过反射去调用。
所以再有人问一个对象中是否可以存在两个同名同参的方法的时候,就可以大胆的说一声可以了。
————————————————
版权声明:本文为CSDN博主「pgaofeng」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zip_tts/article/details/112349732
网友评论