持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天
今天在调试代码的时候出现一个诡异的crash,堆栈如下:
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'boolean
kotlinx.coroutines.flow.MutableStateFlow.tryEmit(java.lang.Object)' on a null object
reference at com.laworks.kotlinconstructtest.EndClass.updateDate(EndClass.kt:21) at
com.laworks.kotlinconstructtest.BaseClass.<init>(BaseClass.kt:16) at
com.laworks.kotlinconstructtest.EndClass.<init>(EndClass.kt:8) at
com.laworks.kotlinconstructtest.MainActivity.onCreate(MainActivity.kt:10)
去看一下代码,这个怎么看都不可能为空啊
private val _firstFlow2 = MutableStateFlow(Date())
val firstFlow2 : StateFlow<Date> = _firstFlow2
override fun updateDate() {
Log.i("EndClass","I'm end class , I will update date")
_firstFlow2.tryEmit(Date())
}
所以怀疑问题可能还是因为初始化的问题。
我有两个类基础关系BaseClass -> EndClass
BaseClass 实现如下:
abstract class BaseClass constructor(val d:Date) {
private val dateFlow = MutableStateFlow(Date())
private val _secondFlow = MutableStateFlow(Date())
val secondFlow : StateFlow<Date> = _secondFlow
init {
updateDate()
}
abstract fun updateDate()
}
BaseClass 有一个抽象的方法,在初始化的时候会调用它(在项目的代码里面可能不是直接调用,可能是通过flow的collect触发),当然这个调用的是子类的实现。EndClass如下:
class EndClass :BaseClass(Date()) {
private val _firstFlow = MutableStateFlow(Date())
val firstFlow : StateFlow<Date> = _firstFlow
private val _firstFlow1 = MutableStateFlow(Date())
val firstFlow1 : StateFlow<Date> = _firstFlow1
private val _firstFlow2 = MutableStateFlow(Date())
val firstFlow2 : StateFlow<Date> = _firstFlow2
override fun updateDate() {
Log.i("EndClass","I'm end class , I will update date")
_firstFlow2.tryEmit(Date())
}
}
从上面的可以看到,所有的成员在初始化的时候就已经实例化了,那为什么还会出现nullpoint呢?这就是kotlin转java过程中初始化顺序的差异了,这个最主要的是EndClass的初始化java代码:
public final class EndClass extends BaseClass {
private final MutableStateFlow _firstFlow = StateFlowKt.MutableStateFlow(new Date());
@NotNull
private final StateFlow firstFlow;
private final MutableStateFlow _firstFlow1;
@NotNull
private final StateFlow firstFlow1;
private final MutableStateFlow _firstFlow2;
@NotNull
private final StateFlow firstFlow2;
@NotNull
public final StateFlow getFirstFlow() {
return this.firstFlow;
}
@NotNull
public final StateFlow getFirstFlow1() {
return this.firstFlow1;
}
@NotNull
public final StateFlow getFirstFlow2() {
return this.firstFlow2;
}
public void updateDate() {
Log.i("EndClass", "I'm end class , I will update date");
this._firstFlow2.tryEmit(new Date());
}
public EndClass() {
super(new Date());
this.firstFlow = (StateFlow)this._firstFlow;
this._firstFlow1 = StateFlowKt.MutableStateFlow(new Date());
this.firstFlow1 = (StateFlow)this._firstFlow1;
this._firstFlow2 = StateFlowKt.MutableStateFlow(new Date());
this.firstFlow2 = (StateFlow)this._firstFlow2;
}
}
大家看,成员变量的初始化放到构造函数里。构造函数在最开始的时候调用了父类的构造函数,我们再看看父类的构造是什么样子的:
public BaseClass(@NotNull Date d) {
Intrinsics.checkNotNullParameter(d, "d");
super();
this.d = d;
this.dateFlow = StateFlowKt.MutableStateFlow(new Date());
this._secondFlow = StateFlowKt.MutableStateFlow(new Date());
this.secondFlow = (StateFlow)this._secondFlow;
this.updateDate();
}
父类在初始化完变量之后调用了updateDate的方法,这个调用的肯定是子类的方法,但是这个时候子类的构造方法还没有执行完成,成员变量还没有得到初始化,那这个时候使用子类的成员变量当然就会crash啦~。
如果试过的同学,可能不一定会crash,因为对于kotlin转java的时候,成员变量的多少也决定成员变量的初始化时间,下面我们把EndClass的成员变量减少:
class EndClass :BaseClass(Date()) {
val firstFlow1 : StateFlow<Date> = _firstFlow1
private val _firstFlow2 = MutableStateFlow(Date())
val firstFlow2 : StateFlow<Date> = _firstFlow2
override fun updateDate() {
Log.i("EndClass","I'm end class , I will update date")
_firstFlow2.tryEmit(Date())
}
}
我们只留了两个成员变量,现在看看java code是什么样子的:
public final class EndClass extends BaseClass {
private final MutableStateFlow _firstFlow2 = StateFlowKt.MutableStateFlow(new Date());
@NotNull
private final StateFlow firstFlow2;
@NotNull
public final StateFlow getFirstFlow2() {
return this.firstFlow2;
}
public void updateDate() {
Log.i("EndClass", "I'm end class , I will update date");
this._firstFlow2.tryEmit(new Date());
}
public EndClass() {
super(new Date());
this.firstFlow2 = (StateFlow)this._firstFlow2;
}
}
成员的一部分实例化放到了构造函数的外面,这个时候再调用updateDate就不会在Crash了,因为java类在初始化的时候成员变量的赋值在构造函数之前!这就是我们总是遇到的灵异事件,刚才还好好的加了一个变量就不行了,但是其实后面都有自己的逻辑在里面,有的时候我们不仅要把bug修了,更总要的是我们要知道是为什么,防止后面再踩坑。
kotlin是高级语言,但是总归它还是要编译成字节码使用的,有的时候了解后面的原理,才能让自己事半功倍。