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

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

 
  1. private var name0: String //报错

  2. private var name1: String = "xiaoming" //不报错

  3. 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 的写法如下:

 
  1. //用于属性延迟初始化

  2. val name: Int by lazy { 1 }

  3.  
  4. //用于局部变量延迟初始化

  5. public fun foo() {

  6. val bar by lazy { "hello" }

  7. println(bar)

  8. }

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

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

 
  1. class TestCase {

  2.  
  3. private val name: Int by lazy { 1 }

  4.  
  5. fun printname() {

  6. println(name)

  7. }

  8.  
  9. }

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

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

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

 
  1. public <init>()V

  2. L0

  3. LINENUMBER 3 L0

  4. ALOAD 0

  5. INVOKESPECIAL java/lang/Object.<init> ()V

  6. L1

  7. LINENUMBER 5 L1

  8. ALOAD 0

  9. GETSTATIC com/rhythm7/bylazy/TestCase$name$2.INSTANCE : Lcom/rhythm7/bylazy/TestCase$name$2;

  10. CHECKCAST kotlin/jvm/functions/Function0

  11. INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;

  12. PUTname com/rhythm7/bylazy/TestCase.name$delegate : Lkotlin/Lazy;

  13. RETURN

  14. L2

  15. LOCALVARIABLE this Lcom/rhythm7/bylazy/TestCase; L0 L2 0

  16. MAXSTACK = 2

  17. 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代码:

 
  1. TestCase() {

  2. name$delegate = LazyKt.lazy((Function0)name$2.INSTANCE)

  3. }

其中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中的:

 
  1. init {

  2. name$delegate = lazy(()->{})

  3. }

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

查看getName()的具体实现:

 
  1. private final getName()I

  2. L0

  3. ALOAD 0

  4. GETFIELD com/rhythm7/bylazy/TestCase.name$delegate : Lkotlin/Lazy;

  5. ASTORE 1

  6. ALOAD 0

  7. ASTORE 2

  8. GETSTATIC com/rhythm7/bylazy/TestCase.$$delegatedProperties : [Lkotlin/reflect/KProperty;

  9. ICONST_0

  10. AALOAD

  11. ASTORE 3

  12. L1

  13. ALOAD 1

  14. INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object;

  15. L2

  16. CHECKCAST java/lang/Number

  17. INVOKEVIRTUAL java/lang/Number.intValue ()I

  18. IRETURN

  19. L3

  20. LOCALVARIABLE this Lcom/rhythm7/bylazy/TestCase; L0 L3 0

  21. MAXSTACK = 2

  22. MAXLOCALS = 4

相当于java代码:

 
  1. private final int getName(){

  2. Lazy var1 = this.name$delegate;

  3. KProperty var2 = this.$$delegatedProperties[0]

  4. return ((Number)var1.getValue()).intValue()

  5. }

  可以看到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类的具体实现:

 
  1. private object UNINITIALIZED_VALUE

  2.  
  3. private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {

  4. private var initializer: (() -> T)? = initializer

  5. @Volatile private var _value: Any? = UNINITIALIZED_VALUE

  6. // final field is required to enable safe publication of constructed instance

  7. private val lock = lock ?: this

  8.  
  9. override val value: T

  10. get() {

  11. val _v1 = _value

  12. if (_v1 !== UNINITIALIZED_VALUE) {

  13. @Suppress("UNCHECKED_CAST")

  14. return _v1 as T

  15. }

  16.  
  17. return synchronized(lock) {

  18. val _v2 = _value

  19. if (_v2 !== UNINITIALIZED_VALUE) {

  20. @Suppress("UNCHECKED_CAST") (_v2 as T)

  21. }

  22. else {

  23. val typedValue = initializer!!()

  24. _value = typedValue

  25. initializer = null

  26. typedValue

  27. }

  28. }

  29. }

  30.  
  31. ......

  32. }

  以上代码的阅读难度就非常低了。
  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(),而namedelegate.getVaule(),而name

    delegate.getVaule()方法的返回结果是对象name$$delegate内部的_value属性值,在getVaule()第一次被调用时会将_value进行初始化,往后都是直接将_value的值返回,从而实现属性值的唯一一次初始化。

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

猜你喜欢

转载自blog.csdn.net/NCTU_to_prove_safety/article/details/87867031