使用 Kotlin 进行开发,对于 latelinit 和 lazy 肯定不陌生。但其原理上的区别,可能鲜少了解过,借着本篇文章普及下这方面的知识。
lateinit
用法
非空类型可以使用 lateinit 关键字达到延迟初始化。
class InitTest() { lateinit var name: String public fun checkName(): Boolean = name.isNotEmpty() }
如果在使用前没有初始化的话会发生如下 Exception。
AndroidRuntime: FATAL EXCEPTION: main Caused by: kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized at com.example.tiramisu_demo.kotlin.InitTest.getName(InitTest.kt:4) at com.example.tiramisu_demo.kotlin.InitTest.checkName(InitTest.kt:10) at com.example.tiramisu_demo.MainActivity.testInit(MainActivity.kt:365) at com.example.tiramisu_demo.MainActivity.onButtonClick(MainActivity.kt:371) ...
为防止上述的 Exception,可以在使用前通过 ::xxx.isInitialized
进行判断。
class InitTest() { lateinit var name: String fun checkName(): Boolean { return if (::name.isInitialized) { name.isNotEmpty() } else { false } } }
Init: testInit():false
当 name 初始化过之后使用亦可正常。
class InitTest() { lateinit var name: String fun injectName(name: String) { this.name = name } fun checkName(): Boolean { return if (::name.isInitialized) { name.isNotEmpty() } else { false } } }
Init: testInit():true
原理
反编译之后可以看到该变量没有 @NotNull 注解,使用的时候要 check 是否为 null。
public final class InitTest { public String name; @NotNull public final String getName() { String var10000 = this.name; if (var10000 == null) { Intrinsics.throwUninitializedPropertyAccessException("name"); } return var10000; } public final boolean checkName() { String var10000 = this.name; if (var10000 == null) { Intrinsics.throwUninitializedPropertyAccessException("name"); } CharSequence var1 = (CharSequence)var10000; return var1.length() > 0; } }
null 则抛出对应的 UninitializedPropertyAccessException。
public class Intrinsics { public static void throwUninitializedPropertyAccessException(String propertyName) { throwUninitializedProperty("lateinit property " + propertyName + " has not been initialized"); } public static void throwUninitializedProperty(String message) { throw sanitizeStackTrace(new UninitializedPropertyAccessException(message)); } private static <T extends Throwable> T sanitizeStackTrace(T throwable) { return sanitizeStackTrace(throwable, Intrinsics.class.getName()); } static <T extends Throwable> T sanitizeStackTrace(T throwable, String classNameToDrop) { StackTraceElement[] stackTrace = throwable.getStackTrace(); int size = stackTrace.length; int lastIntrinsic = -1; for (int i = 0; i < size; i++) { if (classNameToDrop.equals(stackTrace[i].getClassName())) { lastIntrinsic = i; } } StackTraceElement[] newStackTrace = Arrays.copyOfRange(stackTrace, lastIntrinsic + 1, size); throwable.setStackTrace(newStackTrace); return throwable; } } public actual class UninitializedPropertyAccessException : RuntimeException { ... }
如果是变量是不加 lateinit 的非空类型,定义的时候即需要初始化。
class InitTest() { val name: String = "test" public fun checkName(): Boolean = name.isNotEmpty() }
在反编译之后发现变量多了 @NotNull 注解,可直接使用。
public final class InitTest { @NotNull private String name = "test"; @NotNull public final String getName() { return this.name; } public final boolean checkName() { CharSequence var1 = (CharSequence)this.name; return var1.length() > 0; } }
::xxx.isInitialized
的话进行反编译之后可以发现就是在使用前进行了 null 检查,为空直接执行预设逻辑,反之才进行变量的使用。
public final class InitTest { public String name; ... public final boolean checkName() { boolean var2; if (((InitTest)this).name != null) { String var10000 = this.name; if (var10000 == null) { Intrinsics.throwUninitializedPropertyAccessException("name"); } CharSequence var1 = (CharSequence)var10000; var2 = var1.length() > 0; } else { var2 = false; } return var2; } }
lazy
用法
lazy 的命名和 lateinit 类似,但使用场景不同。其是用于懒加载,即初始化方式已确定,只是在使用的时候执行。而且修饰的只是能是 val 常量。
class InitTest { val name by lazy { "test" } public fun checkName(): Boolean = name.isNotEmpty() }
lazy 修饰的变量可以直接使用,不用担心 NPE。
Init: testInit():true
原理
上述是 lazy 最常见的用法,反编译之后的代码如下:
public final class InitTest { @NotNull private final Lazy name$delegate; @NotNull public final String getName() { Lazy var1 = this.name$delegate; return (String)var1.getValue(); } public final boolean checkName() { CharSequence var1 = (CharSequence)this.getName(); return var1.length() > 0; } public InitTest() { this.name$delegate = LazyKt.lazy((Function0)null.INSTANCE); } }
所属 class 创建实例的时候,实际分配给 lazy 变量的是 Lazy 接口类型,并非 T 类型,变量会在 Lazy 中以 value 暂存,当使用该变量的时候会获取 Lazy 的 value 属性。
Lazy 接口的默认 mode 是 LazyThreadSafetyMode.SYNCHRONIZED,其默认实现是 SynchronizedLazyImpl,该实现中 _value 属性为实际的值,用 volatile 修饰。
value 则通过 get() 从 _value 中读写,get() 将先检查 _value 是否尚未初始化
已经初始化过的话,转换为 T 类型后返回
反之,执行同步方法(默认情况下 lock 对象为 impl 实例),并再次检查是否已经初始化:
已经初始化过的话,转换为 T 类型后返回
反之,执行用于初始化的函数 initializer,其返回值存放在 _value 中,并返回
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer) 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 } } } override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) }
总之跟 Java 里双重检查懒汉模式获取单例的写法非常类似。
public class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
lazy 在上述默认的 SYNCHRONIZED mode 下还可以指定内部同步的 lock 对象。
val name by lazy(lock) { "test" }
lazy 还可以指定其他 mode,比如 PUBLICATION
,内部采用不同于 synchronized
的 CAS
机制。
val name by lazy(LazyThreadSafetyMode.PUBLICATION) { "test" }
lazy 还可以指定 NONE
mode,线程不安全。
val name by lazy(LazyThreadSafetyMode.NONE) { "test" }
the end
lateinit 和 lazy 都是用于初始化场景,用法和原理有些区别,做个简单总结:
lateinit 用作非空类型的初始化:
在使用前需要初始化
如果使用时没有初始化内部会抛出 UninitializedPropertyAccess Exception
可配合 isInitialized 在使用前进行检查
lazy 用作变量的延迟初始化:
定义的时候已经明确了 initializer 函数体
使用的时候才进行初始化,内部默认通过同步锁和双重校验的方式返回持有的实例
还支持设置 lock 对象和其他实现 mode