Reputation: 5421
Let's consider sealed class that's an enum replacement:
sealed class Animal(val key: String) {
object Bear : Animal("bear")
object Fox : Animal("fox")
object Deer : Animal("deer")
init {
println("Animal::init. key : $key")
}
companion object {
val values = listOf(Bear,Fox,Deer)
init {
println("Animal.Companion::init")
}
fun valueOf(key: String) = values.firstOrNull { key == it.key }
}
}
fun main() {
val fox = Animal.Fox // <- problem!
println("Values are: " + Animal.values.joinToString())
val findOut = Animal.valueOf("deer")
println("found: $findOut")
}
Without the problematic line (or, to be precise, when the first access is to values
), everything works as expected:
Animal::init. key : bear
Animal::init. key : fox
Animal::init. key : deer
Animal.Companion::init
Values are: Animal$Bear, Animal$Fox, Animal$Deer
found: Animal$Deer
But if the first use is direct reference to one of the object
s, this particular entry is initialized last, out of textual order, thus leaving null
in the supposedly non-null list and inevitable NPE:
Animal::init. key : bear
Animal::init. key : deer
Animal.Companion::init
Animal::init. key : fox
Values are: Animal$Bear, null, Animal$Deer
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Animal.getKey()" because "it" is null
at Animal$Companion.valueOf (File.kt:24)
at FileKt.main (File.kt:36)
at FileKt.main (File.kt:-1)
What rule am I breaking here?
How to guarantee such construction won't cause this problem?
I'm tagging this specifically Kotlin, because it doesn't seem to be easily reproducible in Java.
Upvotes: 1
Views: 48
Reputation: 15763
The issue is with the companion object. It is only created when directly accessed (why your first example works) or when the Animal
class is accessed (i.e. by the object creation of its child Fox
).
When you start with Animal.Fox
the order of things happening is this:
Fox
: StartAnimal("fox")
: Startval values = listOf(Bear, Fox, Deer)
: StartBear
is created, printing Animal::init. key : bearFox
is already in the state of being created (see step 1.), but no object is yet available, so the reference for Fox
is null
.Deer
is created, printing Animal::init. key : deerval values = listOf(Bear, Fox, Deer)
: FinishAnimal("fox")
: Printing Animal::init. key : foxAnimal("fox")
: FinishedFox
: FinishedThe companion object's values
now contains null
instead of the Fox
object, hence the NPE when you try to access that object.
That obviously shouldn't happen and seems to be a bug in Kotlin.
You cannot fix that yourself, you can work around that, however: If you move the content of your companion object to extension functions, you can remove it from the Animal
class (you still need an empty companion object
for the extension functions to work, though), and the initialization order will always be clean, without trying to access an object that isn't fully created yet:
val Animal.Companion.values: List<Animal>
get() = listOf(Bear, Fox, Deer)
fun Animal.Companion.valueOf(key: String) = values.firstOrNull { key == it.key }
As a rule of thumb, you should avoid situations where a parent (in this case Animal
, respectively its companion object) accesses its children. When this is done in the wrong order it leads to situations like yours.
Upvotes: 1
Reputation: 273540
At its core, this is very similar to this situation. It is all about class initialisation, not be confused about initialing an instance of a class. The key behaviour here is that when an event triggers the initialisation of a class, but that class is already in the process of initialisation nothing will be done.
By accessing Animal.Fox
first, the first class to be initialised is Animal.Fox
, triggered by accessing the singleton instance of the Animal.Fox
class, which is stored in a static final field. Without that access, the first class to be initialised is Animal
, triggered by accessing its companion object. This makes a big difference.
Let's consider the expected case first.
Animal
is triggered by the access to its companion object, which is stored in a static final field.Animal
, the static final field storing the companion object is initialised to a new instance of Animal.Companion
.values
property is initialised. This involves accessing the singleton instances of Fox
, Bear
and Deer
, causing those 3 classes to be initialised too.Animal
, but we are already in the process of initialising Animal
! Consequently nothing further is done. If the subclass constructor tries to access a static field in Animal
, it could very well see an uninitialised value. But you are not doing that, so this is fine.What happens when Animal.Fox
is initialised first?
The initialisation of Animal.Fox
triggers the initialisation of its superclass, Animal
, and things work the same way as before, until you initialise values
. The initialiser of values
accesses the singleton instance of Animal.Fox
, but we are still in the middle of initialising that class, so nothing is done! The static field storing the singleton instance has not been initialised and is still null. After all, this whole chain of class initialisation is started by accessing that field!
One way to fix this is to make values
lazily initialised
companion object {
val values by lazy { listOf(Bear,Fox,Deer) }
}
This way, the constructor call of Animal.Companion
will not trigger the initialisation of the animal subclasses.
Here is the Java code that the Kotlin code roughly translates to. You can easily verify that it has the same problem. I have used List.of
to replace Kotlin's listOf
, so that it throws an exception when there is a null value. You can look at the stack trace to see exactly how it got there.
public class Animal {
private final String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal init: " + name);
}
public String getName() {
return name;
}
public static class Bear extends Animal {
private Bear() { super("bear"); }
public static final Bear INSTANCE = new Bear();
}
public static class Fox extends Animal {
private Fox() { super("fox"); }
public static final Fox INSTANCE = new Fox();
}
public static class Deer extends Animal {
private Deer() { super("deer"); }
public static final Deer INSTANCE = new Deer();
}
public static class Companion {
private Companion() { }
private List<Animal> values = List.of(Bear.INSTANCE, Fox.INSTANCE, Deer.INSTANCE);
public List<Animal> getValues() {
return values;
}
}
public static final Companion COMPANION = new Companion();
}
Upvotes: 2