能說一說 Kotlin 中 lateinit 和 lazy 的區別嗎?
使用 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
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
references
- 面試題:Android 中 Intent 採用了什麼設計模式?
- 終於理解~Android 模組化裡的資源衝突
- 跳槽、換房、堅持輸出,與你分享我匆忙的 2022~
- 別搞錯了,nonTransitiveRClass 不能解決資源衝突!
- 從 internal 修飾符一探 kotlin 的可見性控制
- 一文吃透 Kotlin 中眼花繚亂的函式家族...
- 開發這麼久,gradle 和 gradlew 啥區別、怎麼選?
- 能說一說 Kotlin 中 lateinit 和 lazy 的區別嗎?
- M1 Pro 折騰一年的收穫:一堆“哇塞”的技巧和 App
- 如何打造車載語音互動:Google Voice Interaction 給你答案
- Android 車機初體驗:Auto,Automotive 傻傻分不清楚?
- 深入分析 Android 系統返回手勢的實現原理
- Android 13 返回導航大變更:返回鍵徹底廢棄 可預見型返回手勢
- 從顯示 Tap 位置的原理一探 Android Input 系統
- Android 巢狀 Intent 的隱患以及解決方案
- Android 13 針對 Intent filters 安全的再加強
- Android 13 新的換行策略和針對日文的優化
- 電子廠裡撂了挑子,我默默自學起了Android|2021 年中總結
- 鴻蒙Harmony談了這麼久,和Android到底啥區別?
- Jetpack新成員SplashScreen:打造全新的App啟動畫面