能說一說 Kotlin 中 lateinit 和 lazy 的區別嗎?

語言: CN / TW / HK

使用 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,內部採用不同於 synchronizedCAS 機制。

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