skip to content

What is the danger of calling an open or abstract member from the constructor (or init block / property initializer) of an abstract base class, and what does Kotlin do at that point?

level: principalimportance: nice to knowfreq 25%

answer

  1. base initialized before subclass
  2. virtual call from constructor hits subclass override too early
  3. fields still 0/null, lateinit throws
  4. warning: 'Calling non-final function in constructor'
  5. fix: final, avoid, or two-phase init()

basics

~20 s

When a base class is being constructed, the subclass part isn't set up yet. If the base constructor calls a method the subclass overrides, that override runs before the subclass's fields exist, so it may see uninitialized values or null.

solid answer

~40 s

During construction the base class is initialized before the subclass body. If an abstract base's constructor, init block, or property initializer calls an open/abstract member that the subclass overrides, dynamic dispatch sends the call to the subclass's override — but the subclass's own properties and init blocks have not run yet, so they hold their default values (null for non-null-asserted references, 0 for numbers) or, for lateinit, throw UninitializedPropertyAccessException. Kotlin even issues an IDE/compiler warning: 'Calling non-final function in constructor'. The safe options are: make such members final, avoid calling overridable members during initialization, or move the work into a separate init()/start() method called after construction completes (two-phase initialization).

code

kotlin · 12 lines
kotlin
abstract class View {
    init { onCreate() }              // virtual call during construction
    open fun onCreate() {}
}

class Screen : View() {
    private val title = "Home"
    override fun onCreate() {
        println(title.length)        // NullPointerException-like: title is null here
    }
}
// Screen()  // override runs before 'title' is assigned

go deeper

for a junior

May not realize the override runs early; expects the subclass value to already be set.

for a middle

Knows base initializes before subclass and that the field is still default, but may not name the warning or fixes.

for a senior

Explains dynamic dispatch during construction, lateinit/null effects, the compiler warning, and two-phase-init mitigation.

for a principal

Ties it to the fragile-base-class problem and to Kotlin's final-by-default rationale, and prescribes API patterns that avoid leaking a partially constructed this.

## Initialization order When you create `Sub()` whose base is `abstract class Base`, the order is: 1. Base primary constructor arguments are evaluated. 2. **Base** property initializers and `init` blocks run, top to bottom. 3. **Sub** property initializers and `init` blocks run. So the base is fully set up *before* the subclass's properties exist. ## The trap Virtual (open/abstract) calls use **dynamic dispatch**: the most-derived override runs even when invoked from the base constructor. Combine that with the order above and the override executes while the subclass's state is still uninitialized. ```kotlin abstract class Base { abstract val size: Int init { println("size = $size") } // calls overridden val getter too early } class Sub : Base() { override val size = 42 } Sub() // prints "size = 0" (Sub.size not assigned yet) ``` Here `size` is backed by a field that is still `0` when `Base.init` runs; for a non-null reference property it would be `null` despite the non-null type, and a `lateinit var` access would throw `UninitializedPropertyAccessException`. ## What Kotlin tells you The compiler/IDE emits the warning **"Calling non-final function in constructor"** (or accessing an open property) precisely because the result is order-dependent and likely wrong. It does not prevent compilation — it's a design smell warning. ## Safe patterns - **Make the member `final`** so no override can change the behavior, eliminating the surprise. - **Don't call overridable members during initialization.** Keep constructors/init blocks dependent only on final state. - **Two-phase init:** finish constructing, then call an explicit `fun start()` / `fun initialize()` after the object is fully built, so all overrides see complete state. - **Pass values via the constructor** instead of pulling them through an overridable property. ## Why principals care This is the classic *fragile base class* hazard and a strong argument for Kotlin's final-by-default design: by closing members unless deliberately opened, the language nudges you away from leaking a half-built `this` to subclass code.

  • Why does a non-null property appear null inside the base init block?
    The backing field hasn't been assigned yet — base init runs before subclass initializers — so it holds the JVM default (null), even though the declared type is non-null.
  • How does final-by-default mitigate this whole class of bug?
    If the member is final, there's no override to dispatch to, so the base constructor always runs the known base behavior — removing the order-dependent surprise. You must deliberately `open` to expose the risk.

It's like asking a half-installed appliance to run a custom program before its parts are plugged in — the program runs but finds empty sockets.

saying these in an interview costs you the question

  • Insisting the base sees the subclass's overridden values during its own init
  • Ignoring the 'Calling non-final function in constructor' warning
  • Claiming a non-null type guarantees non-null inside the base constructor
  • Not knowing lateinit access throws UninitializedPropertyAccessException there
  • Having no mitigation (final / two-phase init / pass via constructor)

context