skip to content

What are the exact restrictions on where `lateinit` can be used, and how do you defer initialization of a primitive like `Int`?

level: middleimportance: must knowfreq 60%

answer

  1. var, non-null, non-primitive, no custom accessors
  2. Primitives banned: null is the unset marker
  3. Delegates.notNull() for Int/Boolean/etc.
  4. notNull throws IllegalStateException
  5. Works on top-level and local vars too

basics

~20 s

lateinit only works on a mutable var with a non-null, non-primitive type and no custom getter/setter. For a primitive like Int, you can't use lateinit; instead use by Delegates.notNull<Int>(), which throws if read before being set.

solid answer

~40 s

`lateinit` is allowed only on: a `var` (not `val`); a non-null type; a non-primitive type (so not `Int`, `Long`, `Double`, `Boolean`, `Char` — these are JVM primitives that can't carry an 'uninitialized' sentinel); a property without a custom getter or setter. It works on member, top-level, and local `var`s, and on constructor-injected fields it does NOT work (constructor params are already initialized). For primitives the idiomatic replacement is the standard delegate `kotlin.properties.Delegates.notNull<Int>()`: `var port: Int by Delegates.notNull()`. Reading it before assignment throws `IllegalStateException` ("Property X should be initialized before get"). The reason `lateinit` excludes primitives is implementation: the compiler uses a `null` reference as the not-yet-initialized marker, and primitives have no null representation on the JVM.

code

kotlin · 11 lines
kotlin
import kotlin.properties.Delegates

class Config {
    lateinit var host: String           // ok: non-null reference
    var timeout: Int by Delegates.notNull()  // primitive -> delegate
}

val c = Config()
c.host = "localhost"
c.timeout = 30
println("${c.host}:${c.timeout}")

go deeper

for a junior

Recalls that lateinit needs a non-null var and can't be a val.

for a middle

Lists every constraint and knows Delegates.notNull() handles primitives.

for a senior

Explains WHY primitives are excluded (null sentinel on JVM) and the differing exception types.

for a principal

Compares lateinit vs notNull vs lazy as initialization contracts and picks the right tool per scenario, weighing inspectability and exception semantics.

## The full rule set for `lateinit` `lateinit` may be applied only when ALL of these hold: - The property is a **`var`** (mutable). A `val` is single-assignment and must be initialized at its declaration/constructor. - The type is **non-null** (`String`, not `String?`). - The type is **not a primitive**: `Int`, `Long`, `Short`, `Byte`, `Double`, `Float`, `Boolean`, `Char` are excluded. - The property has **no custom getter or setter** — `lateinit` needs the default backing-field accessors. - It is **not** a constructor parameter (those are already assigned). It CAN be used on: member properties, top-level properties, and local `var`s inside a function body. ## Why primitives are excluded Under the hood, the compiler represents a not-yet-assigned `lateinit` reference using the JVM `null` value, and the generated accessor checks for `null` to decide whether to throw `UninitializedPropertyAccessException`. JVM primitives (`int`, `boolean`, …) cannot be `null` and have a default like `0`/`false`, so there is no way to distinguish "unset" from "legitimately zero". Hence primitives are forbidden. ## The primitive workaround: `Delegates.notNull()` ```kotlin import kotlin.properties.Delegates class Server { var port: Int by Delegates.notNull() fun configure(p: Int) { port = p } } val s = Server() // println(s.port) // IllegalStateException: Property port should be initialized before get. s.configure(8080) println(s.port) // 8080 ``` Note the differences from `lateinit`: - It uses a **delegate** (`by`), not a modifier. - Early access throws `IllegalStateException`, not `UninitializedPropertyAccessException`. - There is **no** `isInitialized` check available for a delegated property this way. ## Summary table | Need | Use | |------|-----| | Defer non-null **object** ref | `lateinit var x: Foo` | | Defer non-null **primitive** | `var x: Int by Delegates.notNull()` | | Compute lazily on first read (val) | `val x by lazy { ... }` |

  • Does `Delegates.notNull()` support an `isInitialized` check like `lateinit`?
    No. The `::prop.isInitialized` syntax is specific to `lateinit` properties; the `notNull` delegate offers no such inspection.
  • Can `lateinit` be on a property with a custom getter?
    No. `lateinit` requires the compiler's default backing-field accessors; a custom getter/setter is incompatible.

saying these in an interview costs you the question

  • Saying you can use lateinit on Int with a cast or trick
  • Not knowing the Delegates.notNull() alternative for primitives
  • Claiming lateinit allows custom getters/setters
  • Confusing UninitializedPropertyAccessException with IllegalStateException as interchangeable here
  • Thinking lateinit can be a constructor parameter

context