Potential null type invariance break

Disclaimer: I am fairly new to the language, so if null invariance is not considered a strict property of the language, or if there is some other Kotlin “rule” I am breaking here please let me know.

I believe I have found an easily reproducible way to break Kotlin’s nullability invariance by utilizing constructor parameter member initialization syntax. As an example, please see below

fun main() {
    // Instantiate with some string
    SubTest("Hello World")
}

// nonNullString is not initialized until after superclass initialization is complete
class SubTest(private val nonNullString: String): Test() {
    override fun doSomething() {
        // Since nonNullString is not a nullable type, this should never throw
        checkNotNull(nonNullString)
    }
}

// Superclass calls the subclass' implementation of a function during its initialization
// (any thing wrong with this?)
abstract class Test {
    private val takeSomething = doSomething()
    abstract fun doSomething()
}

Output:
Exception in thread “main” java.lang.IllegalStateException: Required value was null.

While the usage pattern is rare, in my opinion it is far from unacceptable and I ran into this naturally in my own project.

Regardless, this is only possible thanks to the ability to declare class members inside the constructor signature and I see it as an easy way to break null type invariance.

This problem isn’t specific to Kotlin; it’s in Java too (and probably other languages).

The problem is invoking an overridden method during construction – when the subclass hasn’t yet been fully constructed, and so its invariants may not yet be established.

(I think the only language change that could prevent this would be to prevent accessing any overrideable methods or properties from constructors – but that would prevent a wide range of valid and useful accesses too, and so language designers have not chosen to do that.)

I think that IntelliJ shows a warning for such cases, and so you shouldn’t be completely unaware of the possibiity.

2 Likes

Great points, I hadn’t considered how easy it would be to create this same problem in other languages. Sadly there is no warning that showed up, but the example I give in a second is a good reason why there should be, or even a potential way to safely remedy this behavior without breaking existing code.

I feel there is an expectation being broken here that is specific to Kotlin. For example, consider the case where “private val” was removed and nonNullString is just a regular argument variable. The variables provided in the constructor signature in every language (by definition of a constructor) are initialized during constructor invocation. I feel that this creates an expectation of consistency that a declared member in the constructor invocation would be initialized at the same time as a non-member variable (i.e. invocation time). I have 0 knowledge of the implementation of this system is kotlin, but I wonder if it is possible to have declared members in the constructor signature be initialized at invocation time, just as they would be initialized if they were regular constructor arguments.

That’s an interesting point in your second paragraph… it’d be nice if we could access the constructor param value of a property declared in the constructor.

FWIW, Kotlin does class initialization slightly differently to Java, which may affect things as well. Java initializes ALL field variables first, and then runs the constructor to populate any final fields that are set in the constructor. Kotlin, on the other hand, does a “top down” initialization, where properties in the constructor are initialized first, then it works its way down the class, initializing properties and running init blocks in the order they’re declared. Here’s a quick example:

public class JavaTest {
    private final String name;

    public JavaTest(String name) {
        this.name = name;
    }

    private final String nameUpperCased = name.toUpperCase(); // <---- no good, won't compile
}
class KotlinTest(private val name: String) {
    private val nameUpperCased = name.uppercase() // all good
}
1 Like

Thanks for your insights @Skater901, honestly it’s really not a super important “feature” per-se, but the break in consistency is what gets me.

yes! this is a well known anti pattern, the subclass is not initialized yet, you can’t rely on anything being initalized.

There’s lots of ways to break stuff with initalization, you don’t even need subclasses:

class Foo {
    val s: String = get()
    val q: String = "lalala"
    fun get() = q
}

fun main() {
   println( Foo().s )
}

This prints null

Generally speaking nothing happens “as the same time” as anything else, every thing happens in some order.

When you write class Foo(val s: String) you’re doing quite a few things:

  1. Declare a member called s in the class
  2. Declare a constructor parameter called s for the constructor
  3. Add an instruction in the constructor which copies the constructor parameter over to the member

Now all the fields of a class come into existence at the same time (it’s just allocating some memory) but assigning values needs to happen at a later time, and in some specific order. Specifically 3 happens after the superclass is constructed.

It’s also easy to create examples where you need the base class to be initialized before the derived class:

class Derived: Base() {
    val s: String = foo
}

abstract class Base {
    val foo = "lalala"
}

here one needs Baseto be initialized before Derived. This works as you’d expect.

But you can create also the example where there’s a cyclical dependency:

class Derived: Base() {
    override val name: String = foo
}

abstract class Base {
    abstract val name: String
    val foo = name
}

Here the compiler is happy, all types check out, yet there’s no possible initialization order to actually construct an object.

Summarizing when wirting classes it’s possible there ends up being quite a bit of code, sometimes slightly hidden, that runs at construction time. The compiler executes things in a specified order. It’s basically impossible to prove if there’s a “correct” order for initialization to be correct.

Generally you shouldn’t read class fields during initialization nor call class methods during initialization.

1 Like

Thanks for your reply! All great points, but I think you misunderstood my “at the same time” statement. I’m not talking about at the same time as in happening instantaneously. I’m saying that in the case of both parameters that are declared members and regular constructor parameters both being initialized during invocation time (i.e. the same time). For example, as someone new to the language it would not be clear that simply adding “val” to a constructor parameter would somehow delay when that variable is initialized.