What is covariant return-type overriding in Kotlin, and what subtle correctness pitfalls arise when an open member is called during a superclass constructor while a subclass override depends on uninitialized state?
answer
- Covariant return: override returns a subtype
- Parameters stay invariant
- Open call in constructor → subclass override runs early
- Sees null / UninitializedPropertyAccessException
- Fix: make final, or defer init
basics
~20 sAn override may return a more specific subtype than the parent (covariant return). Danger: if a parent constructor calls an open method, the subclass override runs before the subclass's own fields are set, so it may see null/zero values.
solid answer
~50 sKotlin supports **covariant return types**: an overriding function (or property getter) may return a subtype of the parent's declared return type — e.g. `override fun copy(): Derived` over `open fun copy(): Base`. Parameter types, by contrast, are invariant. The classic correctness pitfall is **calling an `open` member from a constructor or initializer**: during superclass initialization the dynamic type is already the subclass, so an overridden method runs *before* the subclass's properties are initialized. The override then observes default/uninitialized values (e.g. a `lateinit` not yet set, throwing `UninitializedPropertyAccessException`, or a `val` still null). IntelliJ flags this as 'Calling non-final function in constructor'. The fix is to avoid open calls in initialization, mark such members `final`, or defer the work to an explicit post-construction `init`-style method. `super` still dispatches statically, but `this`-bound open calls in constructors are the trap.
code
kotlin · 9 linesopen class Base {
init { setup() } // BAD: open call in constructor
open fun setup() {}
}
class Child : Base() {
private val name: String = "child"
override fun setup() = println(name.length) // NPE-like: name not set yet
}
// Fix: mark setup() final, or call it after construction completes.go deeper
May not know covariant returns or the constructor trap at all.
Knows covariant return exists; may not foresee the uninitialized-state pitfall.
Explains covariant return and recognizes the open-call-in-constructor bug and how to avoid it.
Designs hierarchies to prevent the trap (final members, deferred init, factories) and reasons about substitutability and API contracts.
## Covariant return types When overriding, Kotlin lets the override **narrow the return type** to a subtype of the parent's return type. This is *covariant return-type overriding*: ```kotlin open class Animal { open fun reproduce(): Animal = Animal() } class Cat : Animal() { override fun reproduce(): Cat = Cat() // narrower return — legal } ``` Callers holding an `Animal` still see `Animal`; callers holding a `Cat` get the precise `Cat`. Parameters remain **invariant** — you cannot widen or narrow a parameter type in an override (that would create an overload, not an override). Property getters follow the same covariant rule on their type. ## The constructor-call pitfall The deeper trap is **dynamic dispatch during construction**. When you create a `Subclass`, the **superclass constructor/initializer runs first**, but the object's runtime type is already `Subclass`. So if the superclass constructor (or a property initializer) calls an `open` member via `this`, Kotlin dispatches to the **subclass override** — which executes *before* the subclass's own properties are initialized. ```kotlin open class Base { open val tag: String = "base" init { println("Base sees: ${describe()}") } // calls overridden describe() open fun describe() = "tag=$tag" } class Derived : Base() { val extra: String = "derived-data" override fun describe() = "tag=$tag extra=$extra" // extra not yet set! } // new Derived(): Base.init runs describe() -> Derived.describe() // but `extra` is still null at that moment -> prints 'extra=null' ``` Consequences: - A non-null `val` can be observed as `null`. - A `lateinit var` throws `UninitializedPropertyAccessException`. - Default-zero numerics appear instead of computed values. IntelliJ/Kotlin tooling warns: **"Calling non-final function in constructor"** and **"Accessing non-final property in constructor"**. ## Mitigations - **Make the member `final`** so no override can be dispatched (`final fun describe()` or a `final override`). - **Avoid open calls in initialization** — do not call overridable members from constructors, `init` blocks, or property initializers. - **Defer to an explicit start/configure method** invoked after the object is fully built. - **Use a factory** that constructs then initializes in a controlled order. ## Why `super` is not the same trap `super.member()` is **statically bound** to the named supertype implementation, so it never re-enters a subclass override. The pitfall is specifically `this`-dispatched `open` calls during construction.
- Why can an override narrow the return type but not the parameter types?Narrowing the return is safe (Liskov: callers get at least what they expect). Changing a parameter type would break substitutability and instead defines an overload, not an override.
- How do you safely run subclass-dependent initialization that a base class triggers?Avoid open calls in constructors; expose an explicit `initialize()`/`start()` method called after the object is fully constructed, or make the member `final`.
Like asking a half-built house to describe its furniture — the rooms (subclass fields) are not furnished yet.
saying these in an interview costs you the question
- Claiming you can change parameter types in an override
- Not recognizing that overrides dispatch during superclass construction
- Calling open methods from init blocks without concern
- Assuming a non-null `val` can never be observed as null