阅读 10233

从原理分析Kotlin的延迟初始化: lateinit var和by lazy

Koltin中属性在声明的同时也要求要被初始化,否则会报错。 例如以下代码:

private var name0: String //报错
private var name1: String = "xiaoming" //不报错
private var name2: String? = null //不报错
复制代码

  可是有的时候,我并不想声明一个类型可空的对象,而且我也没办法在对象一声明的时候就为它初始化,那么这时就需要用到Kotlin提供的延迟初始化
  Kotlin中有两种延迟初始化的方式。一种是lateinit var,一种是by lazy

lateinit var

private lateinit var name: String
复制代码

  lateinit var只能用来修饰类属性,不能用来修饰局部变量,并且只能用来修饰对象,不能用来修饰基本类型(因为基本类型的属性在类加载后的准备阶段都会被初始化为默认值)。
  lateinit var的作用也比较简单,就是让编译期在检查时不要因为属性变量未被初始化而报错。
  Kotlin相信当开发者显式使用lateinit var 关键字的时候,他一定也会在后面某个合理的时机将该属性对象初始化的(然而,谁知道呢,也许他用完才想起还没初始化)。

by lazy

  by lazy本身是一种属性委托。属性委托的关键字是by。by lazy 的写法如下:

//用于属性延迟初始化
val name: Int by lazy { 1 }

//用于局部变量延迟初始化
public fun foo() {
    val bar by lazy { "hello" }
    println(bar)
}
复制代码

  以下以name属性为代表来讲解by kazy的原理,局部变量的初始化也是一样的原理。
  by lazy要求属性声明为val,即不可变变量,在java中相当于被final修饰。
  这意味着该变量一旦初始化后就不允许再被修改值了(基本类型是值不能被修改,对象类型是引用不能被修改)。{}内的操作就是返回唯一一次初始化的结果。
  by lazy可以使用于类属性或者局部变量。

  写一段最简单的代码分析by lazy的实现:

class TestCase {

   private val name: Int by lazy { 1 }

   fun printname() {
       println(name)
   }

}
复制代码

  在IDEA中点击toolbar中的 Tools -> Kotlin -> Show Kotlin ByteCode, 查看编辑器右侧的工具栏:

查看字节码
不想看字节码分析的可以直接跳过,每段字节码后面都有java/kotlin版本的解释

更完整的字节码片段如下:

public <init>()V
 L0
  LINENUMBER 3 L0
  ALOAD 0
  INVOKESPECIAL java/lang/Object.<init> ()V
 L1
  LINENUMBER 5 L1
  ALOAD 0
  GETSTATIC com/rhythm7/bylazy/TestCase$name$2.INSTANCE : Lcom/rhythm7/bylazy/TestCase$name$2;
  CHECKCAST kotlin/jvm/functions/Function0
  INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
  PUTname com/rhythm7/bylazy/TestCase.name$delegate : Lkotlin/Lazy;
  RETURN
 L2
  LOCALVARIABLE this Lcom/rhythm7/bylazy/TestCase; L0 L2 0
  MAXSTACK = 2
  MAXLOCALS = 1
复制代码

  该段代码是在字节码生成的public <clinit>()V 方法内的。之所以是在该方法内,是因为非单例object的Kotlin类的属性初始化代码语句经过编译器处理后都会被收集到该方法内,如果是object对象,对应的属性初始化代码语句则会被收集到static <clinit>()V方法中。另外,在字节码中,这两个方法是拥有不同方法签名的,这与语言级别上判断两个方法是否相同的方式有所不同。前者是实例构造方法,后者是类构造方法。
  L0与L1之间的字节码代表调用了Object()的构造方法,这是默认的父类构造方法。L2之后的是本地变量表说明。L1与L2之间的字节码对应如下kotlin代码:

 private val name: Int by lazy { 1 }
复制代码

L1与L2之间这段字节码的意思是:
源代码行号5对应字节码方法体内的行号1; 将this(非静态方法默认的第一个本地变量)推送至栈顶;
获取静态变量com.rhythm7.bylazy.TestCase$name$2.INSTANCE;
检验INSTANCE能否转换为kotlin.jvm.functions.Function0类;
调用静态方法kotlin.LazyKt.lazy(kotlin.jvm.functions.Function0),将INSTANCE作为参数传入,并获得一个kotlin.Lazy类型的返回值;
将以上返回值赋值给com.rhythm7.bylazy.TestCase.name$delegate;
最后结束方法。

相当于java代码:

TestCase() {
    name$delegate = LazyKt.lazy((Function0)name$2.INSTANCE)
}
复制代码

其中name$delegate是编译后生成的属性,对象类型为Lazy。

  private final Lkotlin/Lazy; name$delegate  
复制代码

name$2都是编译后生成的内部类。

final class com/rhythm7/bylazy/TestCase$name$2 extends kotlin/jvm/internal/Lambda  implements kotlin/jvm/functions/Function0
复制代码

  name$2继承了kotlin.jvm.internal.Lambda类并实现了kotlin.jvm.functions.Function0接口, 可以看出name$2其实就是kotlin函数参数类型()->T的具体实现,通过字节码分析不难知道name$2.INSTANCE则是该实现类的一个静态对象实例。
所以以上字节码又相当于Koltin中的:

init {
    name$delegate = lazy(()->{})
}
复制代码

  然而,这些代码的作用仅仅是给一个编译期生成的属性变量赋值而已,并没有其他的操作。
  真正实现属性变量延迟初始化的地方其实是在属性name的getter方法里。
  如果在java代码中调用过kotlin代码,会发现java代码中只能通过setter或getter的方式访问koltin编写的对象属性,这是因为kotlin中默认会对属性添加private修饰符,并根据该属性变量是val还是var生成getter或getter和setter一起生成。然后又根据对该属性的访问权限给getter和setter添加对应的访问权限修饰符(默认是public)。

查看getName()的具体实现:

private final getName()I
 L0
  ALOAD 0
  GETFIELD com/rhythm7/bylazy/TestCase.name$delegate : Lkotlin/Lazy;
  ASTORE 1
  ALOAD 0
  ASTORE 2
  GETSTATIC com/rhythm7/bylazy/TestCase.$$delegatedProperties : [Lkotlin/reflect/KProperty;
  ICONST_0
  AALOAD
  ASTORE 3
 L1
  ALOAD 1
  INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object;
 L2
  CHECKCAST java/lang/Number
  INVOKEVIRTUAL java/lang/Number.intValue ()I
  IRETURN
 L3
  LOCALVARIABLE this Lcom/rhythm7/bylazy/TestCase; L0 L3 0
  MAXSTACK = 2
  MAXLOCALS = 4
复制代码

相当于java代码:

private final int getName(){
    Lazy var1 = this.name$delegate;
    KProperty var2 = this.?delegatedProperties[0]
    return ((Number)var1.getValue()).intValue()
}
复制代码

  可以看到name的getter方法其实是返回了 name$delegate.getValue()方法。?delegatedProperties是编译后自动生成的属性,但在此处并没有用到,所以不用关心。

  那么现在我们要关心的就只有name$delegate.getValue(),也就是Lazy类getValue()方法的具体实现了。

先看LazyKt.lazy(()->T)的实现:

public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
复制代码

再看SynchronizedLazyImpl类的具体实现:

private object UNINITIALIZED_VALUE

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @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
            }

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

......
}
复制代码

  以上代码的阅读难度就非常低了。
  SynchronizedLazyImpl继承了Lazy类,并指定了泛型类型,然后重写了Lazy父类的getValue()方法。 getValue()方法中会对_value是否已初始化做判断,并返回_value,从而实现value的延迟初始化的作用。
  注意,对value的初始化行为本身是线程安全的。

总结

  总结一下,当一个属性name需要by lazy时,具体是怎么实现的:

  1. 生成一个该属性的附加属性:name?delegate;
  2. 在构造器中,将使用lazy(()->T)创建的Lazy实例对象赋值给name?delegate;
  3. 当该属性被调用,即其getter方法被调用时返回name?delegate.getVaule(),而name?delegate.getVaule()方法的返回结果是对象name?delegate内部的_value属性值,在getVaule()第一次被调用时会将_value进行初始化,往后都是直接将_value的值返回,从而实现属性值的唯一一次初始化。

那么,再总结一下,lateinit var和by lazy哪个更好用?
  首先两者的应用场景是略有不同的。
  然后,虽然两者都可以推迟属性初始化的时间,但是lateinit var只是让编译期忽略对属性未初始化的检查,后续在哪里以及何时初始化还需要开发者自己决定。
  而by lazy真正做到了声明的同时也指定了延迟初始化时的行为,在属性被第一次被使用的时候能自动初始化。但这些功能是要为此付出一丢丢代价的。