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

Posted 初一十五啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官:能说一说 Kotlin 中 lateinit 和 lazy 的区别吗?相关的知识,希望对你有一定的参考价值。

使用 Kotlin 进行开发,对于 latelinitlazy 肯定不陌生。但其原理上的区别,可能鲜少了解过,借着本篇文章普及下这方面的知识。

为此还特意去整理了一份关于kotlin相关的文档。
涉及:对象.类.继承.变量.常量.拓展.函数等二十多个内容

一丶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 暂存,当使用该变量的时候会获取 Lazyvalue 属性。
Lazy 接口的默认 modeLazyThreadSafetyMode.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 还可以指定 NONE mode,线程不安全。

     val name by lazy(LazyThreadSafetyMode.NONE) 
         "test"
     

三丶总结

lateinitlazy 都是用于初始化场景,用法和原理有些区别,做个简单总结:
lateinit 用作非空类型的初始化:

  • 在使用前需要初始化
  • 如果使用时没有初始化内部会抛出 UninitializedPropertyAccess Exception
  • 可配合 isInitialized 在使用前进行检查

lazy 用作变量的延迟初始化:

  • 定义的时候已经明确了 initializer 函数体
  • 使用的时候才进行初始化,内部默认通过同步锁和双重校验的方式返回持有的实例
  • 还支持设置 lock 对象和其他实现 mode

为此还特意去整理了一份关于kotlin相关的文档。
涉及:对象.类.继承.变量.常量.拓展.函数等二十多个内容

💡 作者:TechMerger
链接:https://juejin.cn/post/7124114861260275742
来源:稀土掘金

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

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

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

面试官:说一说Zookeeper中Leader选举机制

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

面试大厂回来后,有一些话想对 Java 后端开发说一说

面试高频题:Spring和SpringMvc父子容器你能说清楚吗