
5.3 lateinit和by lazy
Kotlin基于Java的空指针提出了一个空安全的概念,即每个属性默认不可为null。在某个类中,如果某些成员变量没办法在一开始就初始化,并且又不想使用可空类型(也就是带“?”的类型)。那么,可以使用lateinit或者by lazy来修饰它。
被lateinit修饰的变量并不是不用初始化,它需要在生命周期流程中进行获取或者初始化。
而lazy()是一个函数,可以接收一个Lambda表达式作为参数,第一次调用时会执行Lambda表达式,以后调用该属性会返回之前的结果。
by lazy是Kotlin属性委托中的延迟属性。例如下面的代码:

执行结果如下:
aaron cafei tony ----------- tony
因为lazy()最后一行返回的值即为str的值,所以以后每次调用str都可以直接返回该值。
下面分析by lazy的源码。
从lazy()开始分析源码:

actual是Kotlin的关键字,表示多平台项目中的一个平台相关实现。
lazy函数的参数是initializer,它是一个函数类型。lazy函数会创建一个SynchronizedLazyImpl类,并传入initializer参数。
下面是SynchronizedLazyImpl的源码:

可以看到SynchronizedLazyImpl实现了Lazy、Serializable接口,它的value属性重载了Lazy接口的value。
Lazy接口的value属性用于获取当前Lazy实例的延迟初始化值。一旦初始化后,它不得在此Lazy实例的剩余生命周期内更改。

所以SynchronizedLazyImpl的value属性只有get()方法,没有set()方法。
value的get()方法会先判断SynchronizedLazyImpl的_value属性是否是UNINITIALIZED_VALUE,不是的话会返回_value的值。
_value使用@Volatile注解标注,相当于在Java中使用volatile修饰_value属性。volatile具有可见性、有序性,因此一旦_value的值修改了,其他线程可以看到其最新的值。
SynchronizedLazyImpl的_value属性存储了initializer的值。
如果_value的值等于UNINITIALIZED_VALUE,则调用initializer来获取值,通过synchronized来保证这个过程是线程安全的。
lazy()方法还有一个实现,比起上面的方法多了一个参数类型LazyThreadSafetyMode。

SYNCHRONIZED使用的是SynchronizedLazyImpl,跟之前分析的lazy()方法是一致的,PUBLICATION使用的是SafePublicationLazyImpl,而NONE使用的是UnsafeLazyImpl。
其中,UnsafeLazyImpl不是线程安全的,而其他都是线程安全的。
SafePublicationLazyImpl使用AtomicReferenceFieldUpdater来保证_value属性的原子操作。毕竟,volatile不具备原子性。

因此,SafePublicationLazyImpl支持同时多个线程调用,并且可以在全部或部分线程上同时进行初始化。但是,如果某个值已由另一个线程初始化,则将返回该值,而不执行初始化。
表5-1表明了三种模式的区别。
表5-1 三种模式的区别

最后,总结一下lateinit和by lazy的区别:
(1)lateinit只能用于修饰变量var,不能用于可空的属性和Java的基本类型。
(2)lateinit可以在任何位置初始化,并且可以初始化多次。
(3)lazy只能用于修饰常量val,并且lazy在SYNCHRONIZED、PUBLICATION模式下是线程安全的。
(4)lazy在第一次被调用时就被初始化,以后调用该属性会返回之前的结果。