Kotlin 初始化踩坑

194 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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是高级语言,但是总归它还是要编译成字节码使用的,有的时候了解后面的原理,才能让自己事半功倍。