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?
answer
- base initialized before subclass
- virtual call from constructor hits subclass override too early
- fields still 0/null, lateinit throws
- warning: 'Calling non-final function in constructor'
- fix: final, avoid, or two-phase init()
basics
~20 sWhen 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 sDuring 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 linesabstract 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 assignedgo deeper
May not realize the override runs early; expects the subclass value to already be set.
Knows base initializes before subclass and that the field is still default, but may not name the warning or fixes.
Explains dynamic dispatch during construction, lateinit/null effects, the compiler warning, and two-phase-init mitigation.
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)