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.
CodePudding user response:
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.
CodePudding user response:
// 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:
The superclass object is initialized as if created by invoking sctor with the specified parameters;
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;
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;
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;
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.