延迟初始化(lazy initialization)—— Kotlin 中的一种机制,对象的属性不是在创建时初始化,而是在首次访问时初始化。根据 JetBrains, 2024 的数据,lateinit 和 lazy 是实现这一策略的两个内置工具。两者都解决了延迟初始化的问题,但在工作机制和应用领域上有根本区别。
要点
延迟初始化—— 是一种模式,类的属性不是在对象构造时获取值,而是在之后按需获取。在 Kotlin 中,这种模式通过两种根本不同的方式实现:lateinit 修饰符和 lazy 委托。
两种机制解决一个共同的问题—— 属性必须在类中存在,但其值在对象创建时要么未知,要么计算过于耗资源而无需执行。根据 Google I/O 2023 的数据,典型的 Android 应用中多达 40% 的属性可以通过延迟初始化进行优化,从而将启动时间减少 15–25%。
lateinit 和 lazy 之间的选择由三个因素决定:属性的可变性(var 或 val)、其生命周期(一次性或多次赋值)以及线程安全要求(单线程或多线程访问)。
第一个也是最常见的场景—— 依赖注入。框架(Dagger、Hilt、Koin)在对象创建后注入依赖,因此属性无法在构造函数中初始化。没有 lateinit,我们就必须将所有依赖声明为 nullable 并在每次使用时进行检查。
第二个场景—— 重型资源:数据库、网络客户端、文件管理器。它们的创建需要时间和内存,因此应仅在实际使用时初始化。lazy 非常适合此类情况,保证一次性创建。
第三种情况—— Android 组件(Activity、Fragment、ViewModel),其生命周期由操作系统管理。依赖于 onCreate、onViewCreated 或 ViewModel 的 init 块的属性无法在构造函数中初始化。
lateinit—— 是 var 属性的修饰符,允许 Kotlin 编译器延迟初始化。编译器不要求在构造函数中赋值,但在每次访问时生成运行时检查:如果属性未初始化,则抛出 UninitializedPropertyAccessException。
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
lateinit 的限制:属性必须声明为 var(而非 val)、非 nullable、非基本类型(Int、Double、Boolean 等)。原因是基本类型被编译为 JVM 基本类型,它们没有「未初始化」状态。对于 nullable 属性,延迟初始化不是必需的:null 已经表示没有值。
要检查 lateinit 属性的状态,使用通过 :: 运算符的内置引用:::propertyName.isInitialized。这是在不冒异常风险的情况下检查属性是否已初始化的唯一安全方法。该检查仅可从同一类或内部类访问,不能从外部代码访问。
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit 在初始化后不增加额外开销:赋值后,访问属性与直接访问字段相同。唯一的成本是赋值前每次读取时的初始化检查。初始化后,JIT 编译器会优化该检查。
一个重要特性:lateinit 属性不能在内联类中使用,也不支持具有自定义 getter/setter 的属性。如果属性需要计算访问—— 请使用 lazy 代替 lateinit。
lazy—— 是一个属性委托,内置于 Kotlin 标准库中。它在首次访问属性时计算值,并为所有后续调用缓存结果。与 lateinit 不同,lazy 仅适用于 val,使属性在初始化后不可变。
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazy 接受一个可选参数 LazyThreadSafetyMode,用于控制线程安全机制。默认使用 SYNCHRONIZED—— 双重检查锁定(Double-Checked Locking),即使在多线程同时访问时也能保证一次性初始化。
PUBLICATION 模式允许并行初始化:多个线程可以同时执行初始化块,但结果只接受第一个完成的。在高竞争情况下比 SYNCHRONIZED 更快,但会增加资源消耗。
NONE 模式完全禁用同步。仅用于保证从单个线程访问的属性。在此模式下,lazy 以最小的开销运行—— 实际上就像直接赋值。
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy 是一次性初始化依赖项的正确选择:仓库、网络客户端、缓存、数据库。val 的语义防止意外覆盖,默认线程安全使代码在多线程环境中安全。lazy 还能正确地与基本类型一起工作,这是 lateinit 无法做到的。
在 Android 中,lazy 常用于通过 by viewModels() 初始化 ViewModel 依赖项或创建 Retrofit 客户端。但请注意:如果 lazy 块捕获了对 Activity 或 Fragment 的引用,可能会导致内存泄漏,因为委托会保持闭包直到属性生命周期结束。
选择 lateinit 还是 lazy—— 不是偏好问题,而是由属性性质决定的架构决策。每种机制解决自己的任务,其应用领域仅部分重叠。
| 标准 | lateinit | lazy |
|---|---|---|
| 属性类型 | 仅 var | 仅 val |
| Nullable | 禁止 | 允许 |
| 基本类型 | 禁止 | 允许 |
| 线程安全 | 不保证 | 默认 SYNCHRONIZED |
| 状态检查 | ::x.isInitialized | 不需要 |
| 错误时的异常 | UninitializedPropertyAccessException | 初始化块中的错误 |
| 缓存 | 不适用 | 一次性计算 |
| Android Binding | View Binding, Data Binding | 不使用 |
| DI 框架 | Dagger, Hilt, Koin | 手动注入 |
当属性在初始化后需要更改或其创建由外部代码管理时,使用 lateinit。典型示例—— Android Activity 中的 View Binding:binding 在 onCreate 中创建,但仍为 var,因为框架不支持此场景的 val。
当属性一次性初始化、计算成本高且值在对象的生命周期内不变时,使用 lazy。经典示例—— 在首次访问仓库时惰性创建 Retrofit 客户端或 Room 数据库。
在一个类中可以同时使用两种机制。例如,lateinit 用于 View Binding,lazy 用于仓库。这是正常实践,反映了不同属性的不同需求。关键是不要混淆语义:不要在需要 val 的地方使用 lateinit,也不要用 lazy 用于需要覆盖的属性。
关于 lateinit 最常见的错误—— 在属性初始化之前访问它。这会导致 UninitializedPropertyAccessException,该异常在编译阶段无法捕获,因为 Kotlin 信任开发者的正确初始化顺序。解决方案—— 在不明确的情况下,始终在访问前通过 ::property.isInitialized 检查状态。
第二个常见问题—— 将 lateinit 用于语义上为 val 的属性。如果值设置一次且不再更改,lazy 是更正确的选择。它使属性不可变,排除意外覆盖,并免费添加线程安全。
第三个错误—— lazy 带有副作用。lazy 初始化块不应更改外部状态或依赖其他 lazy 属性的初始化顺序,因为计算顺序取决于首次访问且可能不明确。如果 lazy 属性相互引用,会导致循环依赖和 StackOverflowError。
第四个问题—— 通过 lazy 在 Android 中导致内存泄漏。如果 lazy 块捕获了对 Activity 或 Fragment 的引用—— 委托保持闭包,垃圾回收器即使在组件销毁后也无法释放它。解决方案—— 仅将 lazy 用于短生命周期对象,或传递 Application 上下文而非 Activity。
第五个典型错误—— 尝试将 lateinit 应用于基本类型。Kotlin 编译器在语法层面阻止这一点,但开发者尝试通过可空包装器绕过限制。这导致不必要的 null 检查,并完全抵消延迟初始化的优势。
常见问题
lateinit—— var 属性的修饰符,允许在构造函数之后初始化。lazy—— val 属性的委托,在首次访问时计算值并缓存。lateinit 不支持基本类型和 nullable,而 lazy 默认是线程安全的。
可以,通过对属性的内置引用:::propertyName.isInitialized。如果属性已初始化,该方法返回 true。这是在使用 lateinit 字段时避免 UninitializedPropertyAccessException 的唯一安全方法。
基本类型—— Int、Double、Boolean 等—— 被编译为 JVM 基本类型(int、double、boolean),它们没有「未初始化」状态。lateinit 使用 null 作为标志,而基本类型不能为 null,因此该机制对于这些类型在物理上无法实现。
默认使用 LazyThreadSafetyMode.SYNCHRONIZED—— 双重检查锁定,保证在多线程访问时一次性初始化。对于单线程场景使用 NONE,对于高竞争使用 PUBLICATION。
当属性在初始化后需要更改或其创建由框架管理时。典型示例—— Android Activity 中的 View Binding:binding 在 onCreate 中创建且必须为 var。对于一次性初始化的 val 依赖项,使用 lazy。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。