Java 转 Kotlin 系列:究竟该不该用 lateinit?

你是如何看待 lateinit?不少同学对它敬而远之,特别是使用 lateinit 踩坑之后。因为被 lateinit 标记的变量,不再接受空安全检查,它的表现更像是一个普通的 Java 变量。也有同学喜欢尽可能的用上它,把 lateinit 作为介于 nonnull 和 nullable 之间的一个状态:对象构造时为 null,在某一个时刻被初始化后一直都是 nonnull,这样属性的不确定性便减少了。

使用 lateinit 的初衷

我曾是一个 lateinit 的坚定支持者。原因之一是 lateinit 属性比 nullable 属性在行为上更可靠 。所谓可靠,即其行为是确定的。当调用 lateinit 变量时,它此时如果没有被初始化,就会抛出UninitializedPropertyAccessException;如果已经初始化了,则操作一定会执行。反看 nullable 变量,你在任一时刻操作它的时候,它都可能不被执行,因为可空变量在任意时刻都可能被置空。这样的行为在排查问题的时候会造成阻碍。为了减少程序运行的不确定性,我更希望尽可能使用 lateinit 代替 nullable。

另一个原因是既然 Kotlin 语言设计者提供这样的关键字,说明是有可用之处的。

使用 lateinit 的坚持

理性分析完,随后我便开始一顿操作。只要是符合以下条件,我就会使用 lateinit 修饰属性:

  • 该属性在对象构造时无法初始化(缺少必要参数),在某个阶段被初始化之后会一直使用。典型的初始化阶段:Activity.onCreate(),自定义模块的 init()

  • 保证对象的调用都在初始化之后

  • 属性无法用空实现代替。

这个策略看起来是没什么问题的,执行的也比较顺利。自测没有问题,测试那边也顺利通过了。但在灰度的期间还是出现了 UninitializedPropertyAccessException

Crash 量也不多,但总还是得解的。Crash 的原因无非就一个:**在初始化 lateinit 属性之前调用了该属性。**而解决方案根据不同情况有两种:

  • 是异常路径导致 ,如 Activity.onCreate() 时数据不正确,需要 finish Activity 不再执行后续初始化代码。此时 Activity 仍然会执行 onDestroy(),而 lateinit 属性没有被初始化。如果 onDestroy() 有对 lateinit 属性的操作,此时就会抛出 UninitializedPropertyAccessException

解决方案 :使用 ::lateinitVar.isInitialized 方法,对异常路径的 lateinit 属性进行判断,如果没有初始化则不操作。

对比 nullable 属性:lateinit 属性会 crash,nullable 属性不会,且和 lateinit 属性加了初始化判断的效果一致。这种场景下 nullable 属性表现的更好。

  • 是代码逻辑结构不正确导致 ,如在某些情况下,上层在调用模块 init() 方法之前,就调用了模块的其他方法。此时抛出 UninitializedPropertyAccessException

解决方案 :调整代码调用逻辑,保证调用模块init()方法之前不调用模块的其他方法。

对比 nullable 属性:lateinit 属性会 crash,nullable 属性不会。但 lateinit 属性会把问题暴露出来,而 nullable 属性会把问题隐藏起来,导致问题难以发现和解决。

开发者对 lateinit 的争论也大多源自于此。支持 lateinit 的开发者,是希望代码有更好的逻辑性;反对 lateinit 的开发者,是希望代码有更好的健壮性。 而对于我来说,我更希望代码有更好的逻辑性,但我也认可"希望代码有更好的健壮性"的想法,就看开发者的取舍了。

这个想法使我坚持使用 lateinit 半年以上。而这一段使用 lateinit 的痛点,也让我开始重新思考 lateinit 的收益。

使用 lateinit 的痛苦

理论和实践都完善了,但使我苦恼的是,UninitializedPropertyAccessException并没有得到高效的解决,而是三头两日时不时的在灰度时冒出来,使我被迫打断当前工作,花上一点时间解决,并延长版本灰度的时间。这不是我想要的效果。

UninitializedPropertyAccessException主要出现这几种场景:

  • 新代码使用了 lateinit 特性,因没有考虑异常路径在测试期间出现 crash;

  • 旧代码重构后对部分属性使用了 lateinit 特性,在复杂的线上环境中出现 crash;

  • 模块内部代码调整/外部调用逻辑调整,如调用时机的调整,导致之前没有问题的代码,在复杂的线上环境中出现 crash。

Kotlin 的 UninitializedPropertyAccessException本质上和 Java 的空指针错误是一样的,都是错误的估计此处对象不可能为空导致的。在 Java 中我们通过增加一堆空判断来解决这个问题,Kotlin 可以使用 nullable 对象。

而 lateinit 通过舍弃空安全机制,把空安全交回到开发者手上(就像 Java 那样)。但在这几个月的实践中,我发现让开发者自己掌控空指针问题,是困难的。

我发现之前我对 lateinit 的思考,缺少了一个很重要的角度:软件工程的角度。 代码是不断迭代的,维护者可能不止一个人,而 lateinit 对空指针问题的保护不足,容易让新的空指针问题出现在代码迭代之后。没有从软件工程的角度去看待问题,导致我对代码的规划过于理想,让代码降低了健壮性。现在我想给 lateinit 增加这样一个观点:lateinit 是一个和软件工程相悖的特性,它不利于软件的健康迭代。

使用 lateinit 的建议

如果你仍想使用 lateinit,那么我建议:

  1. 充分考虑异常分支的执行情况;

  2. 充分考虑异常时序的执行情况;

  3. 充分考虑代码稳定性,是否容易发生需求变更导致结构调整。

目前依然有典型的 lateinit 适用场景,如Activity.onCreate()初始化的属性。但是不要忘了如果有可能初始化失败,需要在异常路径onDestroy()上增加::lateinitVar.isInitialized判断。

对于 Fragment,如果在onCreate执行了 finish(),它的异常路径会是onCreateView()onViewCreate()onDestroy()

相关推荐
CYRUS_STUDIO1 小时前
利用 Linux 信号机制(SIGTRAP)实现 Android 下的反调试
android·安全·逆向
CYRUS_STUDIO2 小时前
Android 反调试攻防实战:多重检测手段解析与内核级绕过方案
android·操作系统·逆向
黄林晴5 小时前
如何判断手机是否是纯血鸿蒙系统
android
火柴就是我6 小时前
flutter 之真手势冲突处理
android·flutter
法的空间6 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
循环不息优化不止6 小时前
深入解析安卓 Handle 机制
android
恋猫de小郭6 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
jctech6 小时前
这才是2025年的插件化!ComboLite 2.0:为Compose开发者带来极致“爽”感
android·开源
用户2018792831676 小时前
为何Handler的postDelayed不适合精准定时任务?
android
叽哥7 小时前
Kotlin学习第 8 课:Kotlin 进阶特性:简化代码与提升效率
android·java·kotlin