Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How are overridden properties handled in init blocks?

Tags:

kotlin

I'm trying to understand why the following code throws:

open class Base(open val input: String) {
  lateinit var derived: String

  init {
    derived = input.toUpperCase() // throws!
  }
}

class Sub(override val input: String) : Base(input)

When invoking this code like this:

println(Sub("test").derived)    

it throws an exception, because at the time toUpperCase is called, input resolves to null. I find this counter intuitive: I pass a non-null value to the primary constructor, yet in the init block of the super class it resolves to null?

I think I have a vague idea of what might be going on: since input serves both as a constructor argument as well as a property, the assignment internally calls this.input, but this isn't fully initialized yet. It's really odd: in the IntelliJ debugger, input resolves normally (to the value "test"), but as soon as I invoke the expression evaluation window and inspect input manually, it's suddenly null.

Assuming this is expected behavior, what do you recommend to do instead, i.e. when one needs to initialize fields derived from properties of the same class?

UPDATE: I've posted two even more concise code snippets that illustrate where the confusion stems from:

https://gist.github.com/mttkay/9fbb0ddf72f471465afc https://gist.github.com/mttkay/5dc9bde1006b70e1e8ba

like image 967
Matthias Avatar asked Jan 09 '16 18:01

Matthias


Video Answer


2 Answers

The original example is equivalent to the following Java program:

class Base {
    private String input;
    private String derived;

    Base(String input) {
        this.input = input;
        this.derived = getInput().toUpperCase();  // Initializes derived by calling an overridden method
    }

    public String getInput() {
        return input;
    }
}

class Derived extends Base {
    private String input;

    public Derived(String input) {
        super(input);    // Calls the superclass constructor, which tries to initialize derived
        this.input = input;  // Initializes the subclass field
    }

    @Override
    public String getInput() {
        return input;   // Returns the value of the subclass field
    }
}

The getInput() method is overridden in the Sub class, so the code calls Sub.getInput(). At this time, the constructor of the Sub class has not executed, so the backing field holding the value of Sub.input is still null. This is not a bug in Kotlin; you can easily run into the same problem in pure Java code.

The fix is to not override the property. (I've seen your comment, but this doesn't really explain why you think you need to override it.)

like image 189
yole Avatar answered Nov 15 '22 11:11

yole


The confusion comes from the fact that you created two storages for the input value (fields in JVM). One is in base class, one in derived. When you are reading input value in base class, it calls virtual getInput method under the hood. getInput is overridden in derived class to return its own stored value, which is not initialised before base constructor is called. This is typical "virtual call in constructor" problem.

If you change derived class to actually use property of super type, everything is fine again.

class Sub(input: String) : Base(input) {
    override val input : String
        get() = super.input
}
like image 36
Ilya Ryzhenkov Avatar answered Nov 15 '22 12:11

Ilya Ryzhenkov