Koltin中属性在声明的同时也要求要被初始化,否则会报错。 例如以下代码:
private var name0: String //报错 private var name1: String = "xiaoming" //不报错 private var name2: String? = null //不报错
可是有的时候,我并不想声明一个类型可空的对象,而且我也没办法在对象一声明的时候就为它初始化,那么这时就需要用到Kotlin提供的 延迟初始化 。
Kotlin中有两种延迟初始化的方式。一种是 lateinit var ,一种是 by lazy 。
private lateinit var name: String
lateinit var只能用来修饰类属性,不能用来修饰局部变量,并且只能用来修饰对象,不能用来修饰基本类型(因为基本类型的属性在类加载后的准备阶段都会被初始化为默认值)。
lateinit var的作用也比较简单,就是让编译期在检查时不要因为属性变量未被初始化而报错。
Kotlin相信当开发者显式使用lateinit var 关键字的时候,他一定也会在后面某个合理的时机将该属性对象初始化的(然而,谁知道呢,也许他用完才想起还没初始化)。
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时,具体是怎么实现的:
那么,再总结一下,lateinit var和by lazy哪个更好用?
首先两者的应用场景是略有不同的。
然后,虽然两者都可以推迟属性初始化的时间,但是lateinit var只是让编译期忽略对属性未初始化的检查,后续在哪里以及何时初始化还需要开发者自己决定。
而by lazy真正做到了声明的同时也指定了延迟初始化时的行为,在属性被第一次被使用的时候能自动初始化。但这些功能是要为此付出一丢丢代价的。