能说一说 Kotlin 中 lateinit 和 lazy 的区别吗?

Posted TechMerger

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了能说一说 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 接口类型,并非 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

以上是关于能说一说 Kotlin 中 lateinit 和 lazy 的区别吗?的主要内容,如果未能解决你的问题,请参考以下文章

面试官:能说一说 Kotlin 中 lateinit 和 lazy 的区别吗

面试官:能说一说 Kotlin 中 lateinit 和 lazy 的区别吗?

第一次领到退休金的你是什么感受,能说一说吗?

Kotlin中,lateinit 和 lazy{} 的区别

Kotlin基础 关键字:lateinit和by lazy

Kotlin基础 3.关键字:lateinit(更新中)