源起是同事的一次反馈,在提测期间报了一个 Kotlin.Lazy 的空指针异常,Lazy 的定义如下:
代码语言:javascript复制class TestA{
...
val xxxx:Service? by lazy{
xxxService()
}
...
}
看起来很平常的 by lazy 为何会报空指针?在深入 lazy 源码查看的时候,并未发现任何可疑点,由于当时的代码逻辑涉及到并发调用,也查看了 by lazy 的初始化,默认实现是 SynchronizedLazyImpl,已经做了线程安全操作。
为了避免太多代码的干扰,我们将涉及到 by lazy 使用的地方都拷贝到了一个 Test 类中,然后通过 Decompile 反编译成 Java 代码来查看是否是 kotlin 的问题。
Kotlin 代码如下:
代码语言:javascript复制class TestA {
init {
....
initView()
}
private fun initView() {
// 调用 Service 方法
service?.getName()
}
private val service: AService? by lazy {
AService()
}
}
反编译后的 Java 代码:
代码语言:javascript复制public final class TestA {
private final Lazy service$delegate;
private final void initView() {
// 1、获取 service 实例
AService var10000 = this.getService();
if (var10000 != null) {
var10000.getName();
}
}
private final AService getService() {
Lazy var1 = this.service$delegate;
Object var3 = null;
// 2、调用 Lazy 的 getValue 方法
.return (AService)var1.getValue();
}
public TestA() {
this.initView();
// 3、初始化 Lazy 实例
.this.service$delegate = LazyKt.lazy((Function0)null.INSTANCE);
}
}
通过代码的反编译立马查到问题:
- 在 TestA 的构造方法中,先执行 initView 方法获取 AService 的实例
- 但 getService 方法中的 Lazy 还没有初始化,却直接调用了 getValue 方法触发空指针异常
- 在 initView 结束之后再做 Lazy 的初始化,这时候已经晚了,异常已经出现了
那如何解决这问题呢?只需将 by lazy 提到了 init 代码块的前面,如下:
代码语言:javascript复制class TestA {
private val service: AService? by lazy {
AService()
}
init {
initView()
}
...
}
反编译结果:
代码语言:javascript复制public final class TestA {
private final Lazy service$delegate;
....
public TestA() {
// 1、初始化 Lazy 实例
.this.service$delegate = LazyKt.lazy((Function0)null.INSTANCE);
// 2、再调用 getService 方法
.this.initView();
}
}
- 构造终于是先初始化 Lazy 对象
- 再调用 initView 方法,这时候方法内的 Lazy.getValue 就能被正常调用了
是不是有点违背常识?为什么在方法里调用一个变量还会涉及到变量放置的位置,Kotlin 这高级语法糖恐怕连 C 都不如吧(嘲笑一番,哈哈)。
那 Kotlin 真的没有对其做语法检查吗?其实是有的,我改变下代码给大家看下:
IDE 会提示当前 service 未初始化,「但该提示仅限在 init 代码块中调用 lazy 的时候提示,如果在 init 中调用一个中间方法,然后再从中间方法调用 lazy,该提示校验将会失效」。
又被 Kotlin 语法糖坑惨的一天!!!