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.
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
}