skip to content

What are the restrictions on what kinds of properties can be declared `lateinit`?

level: middleimportance: must knowfreq 70%

answer

  1. var only, no val
  2. Non-null type, no ?
  3. No primitives (need null sentinel)
  4. No custom get/set
  5. Not constructor params; Delegates.notNull for primitives

basics

~10 s

It must be a var, not a val. It can't be a nullable type, can't be a primitive like Int or Boolean, and can't have a custom getter or setter.

solid answer

~40 s

`lateinit` is allowed only on a mutable `var` (never `val`), declared with a **non-null** type. It cannot be applied to **primitive types** — `Int`, `Long`, `Short`, `Byte`, `Double`, `Float`, `Boolean`, `Char` — because the runtime needs a sentinel `null` to detect the uninitialized state, and primitives can't be null on the JVM. The property must have **no custom getter or setter** (the compiler-generated accessors carry the init check). It works on member properties, top-level properties, and local variables; it does **not** work on constructor parameters (those are already initialized). Generic type parameters can't be `lateinit` unless bounded to a non-null reference type. For deferred primitives or `val`, use `by lazy` or a nullable type with a default instead.

code

kotlin · 12 lines
kotlin
import kotlin.properties.Delegates

class Config {
    lateinit var host: String          // OK: non-null reference var
    // lateinit val port: Int          // 2 errors: val AND primitive

    var port: Int by Delegates.notNull()  // primitive replacement; throws if read early
}

val c = Config()
c.host = "localhost"
c.port = 8080

go deeper

for a junior

Knows it must be a var and not nullable, even if fuzzy on primitives and accessors.

for a middle

Lists var-not-val, non-null, no-primitives, no-custom-accessors, and can explain why primitives are excluded.

for a senior

Adds the null-sentinel rationale, generic constraints, and names Delegates.notNull / by lazy as targeted replacements.

for a principal

Reasons about which workaround fits which scenario and the API-design implications of choosing lateinit vs lazy vs nullable across a codebase.

## The full restriction list A property declared `lateinit` must satisfy **all** of these: ### 1. Must be `var`, never `val` `lateinit val` does not compile. A `val` is assign-once and must be definitely assigned at construction; allowing deferred init would break the read-only guarantee. Use `by lazy` for a deferred `val`. ```kotlin lateinit var ok: String // compiles // lateinit val bad: String // error: 'lateinit' modifier is not allowed on properties of a 'val' ``` ### 2. Non-null type only The type must not be nullable (`String?` is rejected). The whole point is to keep a non-null type while deferring assignment. ### 3. No primitive (value) types Forbidden: `Int`, `Long`, `Short`, `Byte`, `Double`, `Float`, `Boolean`, `Char`. ```kotlin // lateinit var count: Int // error: 'lateinit' modifier is not allowed on properties of primitive types ``` **Why:** the JVM backing field for a `lateinit` reference starts as `null` and the generated check tests for `null`. Primitives can't be `null` on the JVM, so there is no sentinel to mark "uninitialized." Boxed reference types (`Integer`/`Number`) aren't what `Int` compiles to here, so the simplest answer is: use `by lazy` or a default value for numbers/booleans. ### 4. No custom getter or setter The compiler-synthesized accessors embed the initialization guard. A custom accessor would have nowhere to host that check. ### 5. Applicable scopes - Member properties of a class. - Top-level properties. - Local variables (since the feature was extended to locals). - **Not** primary-constructor parameters — those are initialized as part of construction. ### 6. Generics caveat A property of a generic type parameter can't be `lateinit` unless the parameter is constrained so the type is a non-null reference. ## What to use instead when a restriction bites | Need | Use | |------|-----| | Deferred `val` | `by lazy { ... }` | | Deferred primitive (`Int`, `Boolean`) | `by lazy`, or nullable + default, or `Delegates.notNull()` | | May legitimately be absent | nullable type `T?` | `kotlin.properties.Delegates.notNull<Int>()` is the idiomatic replacement for a non-null primitive that's set later — it throws `IllegalStateException` if read before being set.

  • How do you get lateinit-like behavior for an `Int`?
    Use `var x: Int by Delegates.notNull()` — it stores the value in a delegate and throws `IllegalStateException` on read before assignment, mimicking the lateinit guard for primitives.
  • Why is `lateinit` not allowed on `val`?
    A `val` is read-only and must be definitely assigned at construction; deferring it would let an unassigned val be 'read-only but unset,' undermining the immutability contract. Use `by lazy` for a deferred val.

saying these in an interview costs you the question

  • Claiming `lateinit val` is valid
  • Saying `lateinit var x: Int` compiles
  • Allowing a nullable lateinit type (`String?`)
  • Thinking it works on constructor parameters
  • Not knowing Delegates.notNull as the primitive workaround

context