Agent_L
Agent_L

Reputation: 5421

Why Kotlin sealed class object's initialization order changes?

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 objects, 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

Answers (2)

tyg
tyg

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:

  1. Creation of Fox: Start
  2. Creation of Animal("fox"): Start
  3. Creation of Animal class initializer (i.e. companion object): Start
  4. val values = listOf(Bear, Fox, Deer): Start
  5. Bear is created, printing Animal::init. key : bear
  6. Fox is already in the state of being created (see step 1.), but no object is yet available, so the reference for Fox is null.
  7. Deer is created, printing Animal::init. key : deer
  8. val values = listOf(Bear, Fox, Deer): Finish
  9. printing Animal.Companion::init
  10. Creation of Animal class initializer (i.e. companion object): Finished
  11. Creation of Animal("fox"): Printing Animal::init. key : fox
  12. Creation of Animal("fox"): Finished
  13. Creation of Fox: Finished

The 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

Sweeper
Sweeper

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.

  1. The initialisation of Animal is triggered by the access to its companion object, which is stored in a static final field.
  2. During the initialisation of Animal, the static final field storing the companion object is initialised to a new instance of Animal.Companion.
  3. As a result, the values property is initialised. This involves accessing the singleton instances of Fox, Bear and Deer, causing those 3 classes to be initialised too.
  4. Initialising those 3 classes would normally trigger the initialisation of their superclass 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

Related Questions