skip to content

What happens if an `init` block (or property initializer) reads a property declared later in the class? Show the failure mode.

level: seniorimportance: should knowfreq 45%

answer

  1. Forward reference = read before init runs
  2. Direct read -> compile error
  3. Indirect via open override -> JVM default (0/null)
  4. Calling open members from init leaks this
  5. lateinit/lazy as safer alternatives

basics

~20 s

Reading a property before its initializer has run gives you trouble: usually the compiler rejects it, but in some cases the code runs and you see a default value like 0 or null instead of the real value.

solid answer

~40 s

Because initialization is strictly top-to-bottom, reading a property *above* its initializer is a **forward reference**. For a direct read, Kotlin's compiler reports an error like 'Variable cannot be accessed before declaration'. But the protection isn't total: when the read happens **indirectly** — e.g. via an open member overridden in a subclass, or through a method called from `init` that touches a not-yet-initialized property — the compiler can't see it, and at runtime you observe the JVM **default value** (`0`, `false`, `null`) rather than the intended one. For non-null reference types this can surface as a `NullPointerException` despite the type being declared non-null, because the backing field is still `null` mid-construction. The cure is to order declarations so every member is initialized before any code reads it, and avoid calling overridable methods from `init`.

code

kotlin · 8 lines
kotlin
open class Base {
    open val tag: String = "base"
    init { check(tag.isNotEmpty()) }   // dispatches to override -> tag is null here!
}
class Derived : Base() {
    override val tag: String = "derived"
}
// new Derived() -> NPE inside Base.init because Derived.tag backing field is still null

go deeper

for a junior

Recognizes that reading a later property in init is wrong and often won't compile.

for a middle

Explains the compile-time error for direct reads and knows ordering must respect dependencies.

for a senior

Explains the indirect-read default-value/NPE trap via open overrides during base construction and avoids leaking this.

for a principal

Designs class hierarchies and uses lazy/lateinit/factory patterns to eliminate construction-order hazards across modules.

## Forward references during construction Initialization runs in declaration order. A **forward reference** is reading a member before its initializer has executed. ### Case 1 — direct read: compile error ```kotlin class A(x: Int) { init { println(y) } // error: variable 'y' must be initialized val y = x + 1 } ``` The compiler performs definite-assignment analysis and rejects the obvious case. ### Case 2 — indirect read via an open/overridden member: default value at runtime This is the dangerous one. When a base class calls an **open** member from its `init`/initializer, and a subclass overrides it, the override runs **while the subclass's own fields are still uninitialized**: ```kotlin open class Base { open val size: Int = 0 init { println("Base sees size=$size") } } class Derived : Base() { override val size: Int = 42 } // new Derived() prints "Base sees size=0" (NOT 42) ``` Why: the JVM constructs `Base` first, so `Base.init` runs before `Derived`'s `size = 42` initializer. The overridden `size` getter is dispatched dynamically to `Derived`, whose backing field is still the JVM default `0`. For a non-null `String`, you'd read `null`, risking a `NullPointerException` even though the type is non-nullable. ## Rules to stay safe - Declare members **above** every place that reads them. - **Never call open/abstract members from `init` or property initializers** — IntelliJ/`detekt` warn about "leaking `this` in constructor". - Prefer `lazy {}` or computing in `init` after dependencies are set, rather than relying on construction order across the inheritance boundary. ## Tools - `lateinit var` defers initialization but throws `UninitializedPropertyException` if read early — a clearer failure than a silent default. - `by lazy {}` computes on first access, sidestepping construction-order issues entirely (but not usable for `var` and runs outside the constructor).

  • Why does the override return a default instead of the real value?
    The base constructor runs before the subclass initializes its fields; the dynamically dispatched override reads its still-default backing field.
  • How does `lateinit` change the failure mode?
    Reading a lateinit property before assignment throws UninitializedPropertyAccessException — a loud, explicit failure instead of a silent default.

Asking a not-yet-hired employee to do a task: directly, HR blocks it; through a temp agency, someone shows up but with no training — the default, useless answer.

saying these in an interview costs you the question

  • Claiming the compiler always prevents forward references
  • Not knowing open members dispatch to the subclass during base construction
  • Saying a non-null type can never be null
  • Calling overridable methods from init without concern
  • Confusing this with thread-safety rather than construction order

context