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, 查看编辑器右侧的工具栏:
更完整的字节码片段如下:
public ()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object. ()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
方法内的。之所以是在该方法内,是因为非单例object的Kotlin类的属性初始化代码语句经过编译器处理后都会被收集到该方法内,如果是object对象,对应的属性初始化代码语句则会被收集到static
方法中。另外,在字节码中,这两个方法是拥有不同方法签名的,这与语言级别上判断两个方法是否相同的方式有所不同。前者是实例构造方法,后者是类构造方法。
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 lazy(initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer)
复制代码
再看SynchronizedLazyImpl
类的具体实现:
private object UNINITIALIZED_VALUE
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy, 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时,具体是怎么实现的:
- 生成一个该属性的附加属性:name$$delegate;
- 在构造器中,将使用lazy(()->T)创建的Lazy实例对象赋值给name$$delegate;
- 当该属性被调用,即其getter方法被调用时返回name$$delegate.getVaule(),而name$$delegate.getVaule()方法的返回结果是对象name$$delegate内部的_value属性值,在getVaule()第一次被调用时会将_value进行初始化,往后都是直接将_value的值返回,从而实现属性值的唯一一次初始化。
那么,再总结一下,lateinit var和by lazy哪个更好用?
首先两者的应用场景是略有不同的。
然后,虽然两者都可以推迟属性初始化的时间,但是lateinit var只是让编译期忽略对属性未初始化的检查,后续在哪里以及何时初始化还需要开发者自己决定。
而by lazy真正做到了声明的同时也指定了延迟初始化时的行为,在属性被第一次被使用的时候能自动初始化。但这些功能是要为此付出一丢丢代价的。