kotlin的 by lazy 和 lateinit 关键字的应用

3,612 阅读5分钟
前言

在kotlin中,一般来说声明属性为非空类型必须在构造函数中初始化( 我们知道,kotlin是默认空安全的,任何属性的声明都必须初始化,只有声明支持可空“?”才能把属性声明为null)。但是这样很不方便(例如:属性可以通过依赖注入来初始化,或者在单元测试的setup方法中初始化)在这种情况下,你不能再构造函数内提供一个非空初始化器。但你仍然想在类体中引用该属性时避免空检查。

为了处理这种情况 在kotlin中 我们有两种解决方法 一种是 lateinit(延迟初始化)修饰符标记,另一种 by lazy(延迟属性)来实现。

1.lateinit
lateinit修饰符只能用于在类体中的属性(不是在柱构造函数中声明的var属性,并且仅当该属性没有自定义getter或setter时),而自Kotlin 1.2起,也可用于顶层属性和局部变量。该属性或变量必须为非空类型,并且不能是原生类型。
class MyTest{
lateinit var test : Test

fun setup{
test = Test()
}

fun test(){
test.method()
}
}
在初始化前访问lateinit属性会抛出一个特定异常,如下:
kotlin.UninitializedPropertyAccessException: lateinit property test has not been initialized
这个异常明确的表示该属性被访问及它没有被初始化的事实;

自kotlin1.2起 给出了一个可以检测lateinit var 是否初始化的方法:.isInitialized:
使用方法如下:
class Aaa{
lateinit var test:CommonImageLoader

fun isTestInit():Boolean{

return ::test.isInitialized
}
}
返回false 是未初始化 true为已经初始化

但是该方法只能在在 lateinit声明位于同一个类里使用 不能再这个类外使用。


在使用lateinit时需要注意:
  1. lateinit 只能作用于var关键字标注的属性
  2. ::test.isInitialized 检查是否初始化只能在和属性位于同一个类里时使用


2.by lazy(延迟属性)
延迟属性会在第一次访问时进行计算,而且这个 只对val 属性起作用
延迟属性时接受了一个lambda并返回一个lazy<T>实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用get()会执行已经传递给lazy()的lambda表达式并记录结果,后续调用get()只是返回记录的结果。
class Aaa{
lateinit var test:Bbb

fun isTestInit():Boolean{

return ::test.isInitialized
}

val test1:Bbb by lazy {
print("bbbbbbbbbbbbb\n")
Bbb("cccccccccccc")
}
}

class Bbb(var bbb:String)

fun main(str:Array<String>){

var aa = Aaa()
while (true) {
if(aa.isTestInit()){
print(aa.test.bbb)
break
}else{
print("................\n")
aa.test = Bbb("aaaaaaa")
}
}
print("\n................\n")
print(aa.test1.bbb)
}

//打印结果
................
aaaaaaa
................
bbbbbbbbbbbbb
cccccccccccc
在lazy的lambda函数里 最后一行必须是 属性类型
在默认情况下,对于lazy属性的值是同步锁定的(synchronized):该值只在一个线程中计算,并且所有的线程看到相同的值。如果初始化委托的同步锁不是必须的,这样多个线程可以同时执行,那么将LazyThreadSafetyMode.PUBLICATION作为参数传给lazy()函数。而如果确定初始化将总发生在单线程中,那么可以将LazyThreadSafetyMode.NONE作为参数传给lazy()函数,它不会有任何线程安全的保证以及相关的开销。

在默认的情况下 是有同步锁的 所以lambda函数只执行一次
代码如下:
class Aaa{
val test1:ArrayList<String> by lazy{
print("bbbbbbbbbbbbb\n")
ArrayList<String>()
}
}

class Bbb(var bbb:String)

fun main(str:Array<String>){

var aa = Aaa()

Thread(){
Thread.sleep(2)
print("\n.........1.......\n")
aa.test1
aa.test1.add("aaa")
}.start()
Thread(){
Thread.sleep(1)
print("\n.........2.......\n")
aa.test1
aa.test1.add("bbb")
}.start()
Thread(){
print("\n.........3.......\n")
aa.test1
aa.test1.add("ccc")
}.start()

Thread.sleep(300)
print("\n test1列表个数:"+aa.test1.size)
}
//打印日志
.........3.......

.........1.......

.........2.......
bbbbbbbbbbbbb

test1列表个数:3

/**
* Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value,
* but only the first returned value will be used as the value of [Lazy] instance.
初始化器函数可以在并发访问未初始化的[延迟]实例值时多次调用,
但是只有第一个返回值将用作[Lazy]实例的值。

*/
LazyThreadSafetyMode.PUBLICATION :在这个模式下 如果多线程同时访问,在访问的时候都没有创建这个实例 lambda函数会多次调用 但是创建的实例只有第一次返回的实例是有效的
如下代码:
class Aaa{
val test1:ArrayList<String> by lazy(LazyThreadSafetyMode.PUBLICATION){
print("bbbbbbbbbbbbb\n")
ArrayList<String>()
}
}

class Bbb(var bbb:String)

fun main(str:Array<String>){

var aa = Aaa()

Thread(){
Thread.sleep(2)
print("\n.........1.......\n")
aa.test1
aa.test1.add("aaa")
}.start()
Thread(){
Thread.sleep(1)
print("\n.........2.......\n")
aa.test1
aa.test1.add("bbb")
}.start()
Thread(){
print("\n.........3.......\n")
aa.test1
aa.test1.add("ccc")
}.start()

Thread.sleep(300)
print("\n test1列表个数:"+aa.test1.size)
}
//打印日志
.........2.......

.........1.......
bbbbbbbbbbbbb
bbbbbbbbbbbbb

.........3.......

test1列表个数:3

/**
* No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined.
*
* This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
不使用锁来同步对[Lazy]实例值的访问;如果从多个线程访问该实例,则其行为未定义。
除非保证[Lazy]实例永远不会从多个线程初始化,否则不应该使用这种模式。
*/
LazyThreadSafetyMode.NONE :在这个模式下 如果多个线程同时访问 在访问的时候都没有创建这个实例的话 会创建多个实例并返回 并且后返回的会覆盖前面返回的实例
如下代码:

class Aaa{

val test1:ArrayList<String> by lazy(LazyThreadSafetyMode.NONE){
print("bbbbbbbbbbbbb\n")
ArrayList<String>()
}
}

class Bbb(var bbb:String)

fun main(str:Array<String>){

var aa = Aaa()

Thread(){
Thread.sleep(2)
print("\n.........1.......\n")
aa.test1
aa.test1.add("aaa")
}.start()
Thread(){
Thread.sleep(1)
print("\n.........2.......\n")
aa.test1
aa.test1.add("bbb")
}.start()
Thread(){
print("\n.........3.......\n")
aa.test1
aa.test1.add("ccc")
}.start()

Thread.sleep(300)
print("\n test1列表个数:"+aa.test1.size)
}
//打印的日志
.........3.......
.........2.......
.........1.......
bbbbbbbbbbbbb
bbbbbbbbbbbbb
bbbbbbbbbbbbb
test1列表个数:1

使用by lazy需要注意:
  1. by lazy 只能作用于val关键字标注的属性
  2. 只有在属性被第一次调用才会运行“lazy{}”里面的内容,第二次在调用时,不会在运行“lazy()”里面的内容
  3. 使用不同的模式时需要注意线程同步问题。