lateinit / lazy:Kotlin 延迟初始化的本质与机制

作者: IT Sectr 发布日期: 2026-06-23 阅读时间: 8 分钟

延迟初始化(lazy initialization)—— Kotlin 中的一种机制,对象的属性不是在创建时初始化,而是在首次访问时初始化。根据 JetBrains, 2024 的数据,lateinit 和 lazy 是实现这一策略的两个内置工具。两者都解决了延迟初始化的问题,但在工作机制和应用领域上有根本区别。

要点

  • lateinit—— var 属性的修饰符,允许在对象创建后初始化
  • lazy—— val 属性的委托,在首次访问时初始化值
  • lateinit 需要 var 且不支持 JVM 基本类型
  • lazy 默认是线程安全的,并缓存计算结果
  • lateinit 在过早访问时抛出 UninitializedPropertyAccessException

Kotlin 中的延迟初始化是什么?

延迟初始化—— 是一种模式,类的属性不是在对象构造时获取值,而是在之后按需获取。在 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:机制与限制

lateinit—— 是 var 属性的修饰符,允许 Kotlin 编译器延迟初始化。编译器不要求在构造函数中赋值,但在每次访问时生成运行时检查:如果属性未初始化,则抛出 UninitializedPropertyAccessException

kotlin
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。这是在不冒异常风险的情况下检查属性是否已初始化的唯一安全方法。该检查仅可从同一类或内部类访问,不能从外部代码访问。

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

    fun isReady(): Boolean {
        return ::binding.isInitialized
    }
}

lateinit 的性能

lateinit 在初始化后不增加额外开销:赋值后,访问属性与直接访问字段相同。唯一的成本是赋值前每次读取时的初始化检查。初始化后,JIT 编译器会优化该检查。

一个重要特性:lateinit 属性不能在内联类中使用,也不支持具有自定义 getter/setter 的属性。如果属性需要计算访问—— 请使用 lazy 代替 lateinit。

lazy:机制与优势

lazy—— 是一个属性委托,内置于 Kotlin 标准库中。它在首次访问属性时计算值,并为所有后续调用缓存结果。与 lateinit 不同,lazy 仅适用于 val,使属性在初始化后不可变。

kotlin
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),即使在多线程同时访问时也能保证一次性初始化。

lazy 的线程安全模式

PUBLICATION 模式允许并行初始化:多个线程可以同时执行初始化块,但结果只接受第一个完成的。在高竞争情况下比 SYNCHRONIZED 更快,但会增加资源消耗。

NONE 模式完全禁用同步。仅用于保证从单个线程访问的属性。在此模式下,lazy 以最小的开销运行—— 实际上就像直接赋值。

kotlin
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
    Config.loadFromFile("config.json")
}

何时 lazy 更优

lazy 是一次性初始化依赖项的正确选择:仓库、网络客户端、缓存、数据库。val 的语义防止意外覆盖,默认线程安全使代码在多线程环境中安全。lazy 还能正确地与基本类型一起工作,这是 lateinit 无法做到的。

在 Android 中,lazy 常用于通过 by viewModels() 初始化 ViewModel 依赖项或创建 Retrofit 客户端。但请注意:如果 lazy 块捕获了对 Activity 或 Fragment 的引用,可能会导致内存泄漏,因为委托会保持闭包直到属性生命周期结束。

lateinit vs lazy:方法对比

选择 lateinit 还是 lazy—— 不是偏好问题,而是由属性性质决定的架构决策。每种机制解决自己的任务,其应用领域仅部分重叠。

标准lateinitlazy
属性类型仅 var仅 val
Nullable禁止允许
基本类型禁止允许
线程安全不保证默认 SYNCHRONIZED
状态检查::x.isInitialized不需要
错误时的异常UninitializedPropertyAccessException初始化块中的错误
缓存不适用一次性计算
Android BindingView Binding, Data Binding不使用
DI 框架Dagger, Hilt, Koin手动注入

当属性在初始化后需要更改或其创建由外部代码管理时,使用 lateinit。典型示例—— Android Activity 中的 View Binding:binding 在 onCreate 中创建,但仍为 var,因为框架不支持此场景的 val。

当属性一次性初始化、计算成本高且值在对象的生命周期内不变时,使用 lazy。经典示例—— 在首次访问仓库时惰性创建 Retrofit 客户端或 Room 数据库。

组合 lateinit 和 lazy

在一个类中可以同时使用两种机制。例如,lateinit 用于 View Binding,lazy 用于仓库。这是正常实践,反映了不同属性的不同需求。关键是不要混淆语义:不要在需要 val 的地方使用 lateinit,也不要用 lazy 用于需要覆盖的属性。

使用 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 检查,并完全抵消延迟初始化的优势。

常见问题

Kotlin 中 lateinit 和 lazy 有什么区别?

lateinit—— var 属性的修饰符,允许在构造函数之后初始化。lazy—— val 属性的委托,在首次访问时计算值并缓存。lateinit 不支持基本类型和 nullable,而 lazy 默认是线程安全的。

能否检查 lateinit 属性是否已初始化?

可以,通过对属性的内置引用:::propertyName.isInitialized。如果属性已初始化,该方法返回 true。这是在使用 lateinit 字段时避免 UninitializedPropertyAccessException 的唯一安全方法。

为什么 lateinit 不能与基本类型一起使用?

基本类型—— Int、Double、Boolean 等—— 被编译为 JVM 基本类型(int、double、boolean),它们没有「未初始化」状态。lateinit 使用 null 作为标志,而基本类型不能为 null,因此该机制对于这些类型在物理上无法实现。

lazy 的默认线程安全模式是什么?

默认使用 LazyThreadSafetyMode.SYNCHRONIZED—— 双重检查锁定,保证在多线程访问时一次性初始化。对于单线程场景使用 NONE,对于高竞争使用 PUBLICATION。

在 Android 中何时使用 lateinit 而不是 lazy?

当属性在初始化后需要更改或其创建由框架管理时。典型示例—— Android Activity 中的 View Binding:binding 在 onCreate 中创建且必须为 var。对于一次性初始化的 val 依赖项,使用 lazy。

总结

  • lateinit—— Kotlin 中 var 属性的修饰符,允许在构造函数后初始化且无需 nullable
  • lazy—— val 的属性委托,一次性计算并自动缓存结果
  • lateinit 在初始化前访问时抛出 UninitializedPropertyAccessException
  • lazy 通过 LazyThreadSafetyMode.SYNCHRONIZED 默认线程安全
  • lateinit 与基本类型和 nullable 属性不兼容
  • lazy 在 Android 中捕获上下文到闭包时可能导致内存泄漏
  • 对于可变属性选择 lateinit,对于一次性初始化的 val 依赖项选择 lazy

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读