使用 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 ?: thisoverride 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

  • Kotlin-lateinit,::xxx.isInitialized的使用
  • Kotlin | 关于 Lazy ,你应该了解的这些事