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

简介: 能说一说 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

相关文章
|
3月前
|
Android开发 Kotlin
Android经典面试题之Kotlin的==和===有什么区别?
本文介绍了 Kotlin 中 `==` 和 `===` 操作符的区别:`==` 用于比较值是否相等,而 `===` 用于检查对象身份。对于基本类型,两者行为相似;对于对象引用,`==` 比较值相等性,`===` 检查引用是否指向同一实例。此外,还列举了其他常用比较操作符及其应用场景。
196 93
|
5月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
182 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
5月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
39 0
|
6月前
|
消息中间件 缓存 API
Kotlin中的StateFlow和SharedFlow有什么区别?
- `StateFlow`持有一个最新的状态值,适合UI状态管理,新订阅者立即收到当前值。 - `SharedFlow`是通用热流,用于事件总线,不保留最新状态但可配置重播。 - `StateFlow`继承自`SharedFlow`,更专注于状态管理,而`SharedFlow`提供事件处理灵活性。 - 示例中展示了如何`emit`新值和`collect`变化。 - 选择`StateFlow`用于单最新状态共享,`SharedFlow`则适用于需要事件历史或定制策略的场景。 关注公众号“AntDream”了解更多内容!
84 1
|
7月前
|
Java Kotlin
[AIGC] Java 和 Kotlin 的区别
[AIGC] Java 和 Kotlin 的区别
|
7月前
|
Java Kotlin
[AIGC] Java 和 Kotlin 的区别
[AIGC] Java 和 Kotlin 的区别
60 0
|
Java 开发者 Kotlin
Kotlin为什么会火起来,有什么特点,跟Java区别
Kotlin为什么会火起来,有什么特点,跟Java区别
108 0
|
Java Kotlin
Kotlin 中初始化块、初始化的顺序、lateinit延迟初始化详解
Kotlin 中初始化块、初始化的顺序、lateinit延迟初始化详解
117 0
|
Java 开发者 Kotlin
Kotlin中的 lateinit 和 by lazy
Kotlin中的 lateinit 和 by lazy
202 0
|
2月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
40 1