Kotlin中by lazy是如何实现懒加载的

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)
}

  SynchronizedLazyImplLazy的一个子类,可以看到在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);
   }
}

  从上面可以看到,创建了一个demoString$delegateLazy对象,然后在getDemoString中实际上委托给了demoString$dalegate#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/Demo$demoString$2.INSTANCE
然后是标号11,调用kotlin/LazyKt.lazy方法,将前面的INSTANCE作为参数,然后标号14将结果赋值给demoString$delegate

  也就是说,构造方法中传入的不是null.INSTANCE,而是Demo$demoString$2.INSTANCE。按照前面说的,Demo$demoString$2.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
}

  可以看到,Demo$demoString$2继承了kotlin.jvm.internal.Lambda并且实现了kotlin.jvm.functions.Function0<java.lang.String>接口,其实lambda表达式最终都会被封装成具体的对象来使用。而INSTANCE是静态常量,在static块中赋值的,所以lambda表达式封装的对象都是通过静态常量INSTANCE来实现的单例以供使用

  其中Function0是个interface,定义在kotlin.jvm.functions中,其中一共有23个接口,从Function0Function22,他们的区别就是参数的个数不同,因为我们by lazy的表达式中没有参数,所以实现的是Function0接口。具体的实现都封装在invoke方法中。

5,附2

  前面有说过,lambda表达是最终会被封装成具体的对象,因为前面的表达式是{ "aa" },是无参数的表达式,因此Demo$demoString$2实现的是Function0接口。

public interface Function0<out R> : Function<R> {
    public operator fun invoke(): R
}

  再看看Demo$demoString$2

$ 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 {};
}

  在Demo$demoString$2中,有两个invoke方法。Object invoke()String invoke(),为什么会有两个呢,为什么又能有两个呢?这跟我们说的重载是不一样的,重载是指参数列表不同,而这两个方法显然是不满足的。

  这是因为在class字节码层面,一个方法的描述是包括返回值和方法名和参数列表的。但是在java语言层面,方法描述是不包含返回值的,因此,在代码中我们是不能这样写的。但是由于我们实现了Function<String>接口,所以会有一个String invoke()方法。另外由于泛型擦除,该方法实际上是Object invoke(),而实现一个接口必须实现他的方法,所以在class中就出现了两个invoke方法。

  Object invoke()属于桥接方法,本身的实现就是直接调用对应的方法,本例中是直接调用String invoke(),这点从上面的字节码中也可以看出。该方法外部无法直接调用,但是可以通过反射去调用。

  所以再有人问一个对象中是否可以存在两个同名同参的方法的时候,就可以大胆的说一声可以了。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值