2

An unexpected NPE shows up in my application when initialising an inheritor of a superclass utilising abstract val functions in its init block, this has me confused. Perhaps someone can explain why this is. FYI I solved the problem momentarily by using abstract functions instead, but I still do not understand why this happens.

My super class simply wraps another component to express the state better. In the init function there is a common enable function which can cause an immediate callback which would access the abstract vals set in the inheriting class. This causes an NPE and I do not know why, since the vals are overridden correctly in the inheriting class. Here is the code with some explaining comments of the issue:

abstract class SomeSuperClass(private val foundation: SomeFoundation) {

  
  private val callback = object : SomeCallback() {
    override fun onAvailable(network: Network) {
      super.onAvailable(network)
      onAvailable() // Accesses the inheritor which can cause an NPE on init
    }

    override fun onLost(network: Network) {
      super.onLost(network)
      onLost()
    }
  }

  init {
    val manager: SomeManager = SomeManager()
    manager.registerCallback(callback) // Can cause an immediate callback, this is probably why the NPE happens rarely, since it does not always cause an immediate callback.
  }

  abstract val onAvailable: () -> Unit

  abstract val onLost: () -> Unit
}
/** Singleton inheritor. */
class SomeInheritingObject private constructor(): SomeSuperClass(foundation  = SomeFoundation()) {
  private val _state: MutableStateFlow<State> = MutableStateFlow(State.Initial)

  val state: StateFlow<State> = _state

  // This overriden val is not allocated when super.init is called, why?
  override val onAvailable: () -> Unit = {
    _state.value = State.Available
  }

  override val onLost: () -> Unit = {
    _state.value = State.Unavailable
  }

  // This is a singleton component
  companion object {
    private val observer: SomeInheritingObject by lazy { SomeInheritingObject() }

    fun getInstance(): SomeInheritingObject = observer
  }
}

I expect the overridden abstract function values to be set in super.init, perhaps they are not. In that case I'd appreciate if someone would refer me to some documentation.

2 Answers2

2

There's an excellent Stackoverflow answer to a somewhat similar question: What's wrong with overridable method calls in constructors?. Here's a quote from that answer:

The superclass constructor runs before the subclass constructor, so the overriding method in the subclass will be invoked before the subclass constructor has run. If the overriding method depends on any initialization performed by the subclass constructor, the method will not behave as expected.

In your case, the superclass constructor calls manager.registerCallback(callback), which, as you point out in your comment, can lead to an immediate call to onAvailable(), overridden by the subclass. The implementation of this method in the subclass accesses the _state property, which has not been initialized yet at that point, because the subclass constructor hasn't run (even though _state is initialized in-place, it's just syntactic sugar - the Kotlin compiler will generate an actual constructor that will initialize _state) - that's what's causing the NPE.

This is a very tricky issue to troubleshoot, so it's recommended to design your classes in such a way that you can avoid calling abstract or open methods in the constructor.

Egor
  • 39,695
  • 10
  • 113
  • 130
1
// This overriden val is not allocated when super.init is called, why?

You are right. The overridden val is not initialised when super.init is called. The initialisation order is specified in the spec here:

When a classifier type is initialized using a particular secondary constructor ctor delegated to primary constructor pctor which, in turn, is delegated to the corresponding superclass constructor sctor , the following happens, in this initialization order:

  1. The superclass object is initialized as if created by invoking sctor with the specified parameters;

  2. Interface delegation expressions are invoked and the result of each is stored in the object to allow for interface delegation, in the order of appearance of delegation declarations in the supertype specifier list;

  3. pctor is invoked using the specified parameters, initializing all the properties declared by its property parameters in the order of appearance in the constructor declaration;

  4. Each property initialization code as well as the initialization blocks in the class body are invoked in the order of appearance in the class body;

  5. ctor body is invoked using the specified parameters. Note: this means that if an init-block appears between two property declarations in the class body, its body is invoked between the initialization code of these two properties.

The initialization order stays the same if any of the entities involved are omitted, in which case the corresponding step is also omitted (e.g., if the object is created using the primary constructor, the body of the secondary one is not invoked).

In your case, there is no secondary constructor or interface delegation, so steps 2 and 5 are omitted. The crucial thing though, is that step 4 occurs after step 1. Everything in the superclass is initialised first, before any of the initialisation code in the subclass is run, i.e. this:

  private val _state: MutableStateFlow<State> = MutableStateFlow(State.Initial)

  val state: StateFlow<State> = _state

  override val onAvailable: () -> Unit = {
    _state.value = State.Available
  }

  override val onLost: () -> Unit = {
    _state.value = State.Unavailable
  }

so while you are still in the superclass init, none of the above are initialised. On the JVM, this means they are null.

I solved the problem momentarily by using abstract functions instead

I suppose you mean:

  override fun onAvailable() {
    _state.value = State.Available
  }

  override fun onLost() {
    _state.value = State.Unavailable
  }

I don't think that actually solves the problem though, because if the callback is called immediately, _state would still be null.

In any case, I would suggest that you redesign your code to avoid using overridable members from places where subclasses are not fully initialised, like in superclass constructors. Those members are very likely to assume that all the members all initialised, like in this case here.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Thank you, and an additional one for pointing out the mistake I made in the fix. You are correct in it not solving the problem. I think I will opt for composition instead of inheritance, meaning I would inject the callbacks into SomeSuperClass and thus remove the issue. Since the callbacks would always be initialised. I'm curious to hear your thoughts on it. – Linus Lindgren Jan 02 '23 at 12:01
  • @LinusLindgren how is the `_state`, which the call backs use, initialised? If they are still in the subclass, you’d get the same problem wouldn’t you? Unless you meant that you have completely gotten rid of the subclasses, of course. – Sweeper Jan 02 '23 at 13:50
  • Thank you for following up on the thread so others can see this as well. I think how they are initialised is clear in the code example, and yes composition does not solve the problem in the end. I opted for a slightly uglier solution to get rid of the initialisation problem. By building an enable function which enables the callback in the inheriting class I can safely reference the values in the subclass. `SubClass().apply { enble() }` This makes sure that the sub-class _state is not referenced during initialisation. – Linus Lindgren Jan 31 '23 at 07:48