skip to content

When would you choose `by lazy` over `lateinit var`, and how do their initialization and threading semantics differ?

level: middleimportance: should knowfreq 64%

answer

  1. lazy = val, self-computed, cached, thread-safe default
  2. lateinit = var, you assign it, non-null, no caching
  3. lateinit forbids nullable & primitive types
  4. lateinit unset read -> UninitializedPropertyAccessException
  5. Check with ::prop.isInitialized (lateinit)

basics

~10 s

Use by lazy when the value is read-only and you want it computed automatically the first time it's needed. Use lateinit var when something external assigns the value later, like a framework injecting it.

solid answer

~40 s

`by lazy` is a `val` whose value is computed by an initializer lambda on first read and then cached; it is thread-safe by default (SYNCHRONIZED). You don't assign it — the block produces the value. `lateinit var` is a non-null `var` that you must assign yourself before use; reading it before assignment throws `UninitializedPropertyAccessException`. It has no initializer, no caching logic, and no thread-safety guarantees. Other differences: `lazy` works with any type including nullable/primitives; `lateinit` cannot be used with primitive types or nullable types and only on `var`. Choose `lazy` for self-computing, read-once expensive values; choose `lateinit` for values set externally (DI, `@BeforeEach` test setup, Android `onCreate`) where you don't have the value at construction. You can query `lateinit` readiness with `::prop.isInitialized`.

code

kotlin · 12 lines
kotlin
class Handler {
    // Self-computing, read-only, thread-safe
    val cache: Map<String, Int> by lazy { buildCache() }

    // Injected by the framework after construction
    lateinit var ctx: Context

    fun render() {
        if (::ctx.isInitialized) use(ctx)   // guard before reading
        println(cache.size)                 // computes on first call
    }
}

go deeper

for a junior

Knows lazy is a self-computing val and lateinit is a var you assign later.

for a middle

Articulates val-vs-var, caching, thread-safety differences, and lateinit's type restrictions and exception.

for a senior

Maps each to concrete scenarios (DI/test setup vs. expensive read-only init) and discusses per-access cost and isInitialized.

for a principal

Considers API/lifecycle design: where deferred init belongs, failure modes of lateinit in concurrent/framework contexts, testability and observability.

## Two tools for 'not ready at construction' Both avoid forcing a value at object-construction time, but they solve different problems. ### `by lazy` ```kotlin val parser: JsonParser by lazy { JsonParser(loadSchema()) } ``` - **`val` only** — write-once, cached. - **Self-initializing**: the lambda computes the value on first read. - **Cached**: subsequent reads return the stored value. - **Thread-safe by default** (`LazyThreadSafetyMode.SYNCHRONIZED`). - Works with **any type**: nullable (`String?`), primitives (`Int`), etc. ### `lateinit var` ```kotlin lateinit var repository: Repository // assigned later by DI/framework ``` - **`var` only**, **non-null**, and **you must assign it** before reading. - **No initializer, no caching** — it's an ordinary field with a deferred-assignment contract. - Reading before assignment throws **`UninitializedPropertyAccessException`**. - **No thread-safety** is provided. - **Restrictions**: cannot be `lateinit` on a **nullable** type or a **primitive** (`Int`, `Boolean`, …) — those have no sentinel for 'uninitialized'. - Readiness check: `if (this::repository.isInitialized) { ... }`. ## Decision guide - Value is **read-only and self-computable** → `by lazy`. - Value is **injected/assigned by something else later** (Spring, Dagger, `@BeforeEach`, Android lifecycle) → `lateinit var`. - Value is **expensive and may never be used** → `by lazy` (computed only on demand). - Value **changes over time / reassigned** → plain `var` or `lateinit var`, not `lazy`. ## Cost notes - `lazy` adds a small per-access overhead: a null/volatile check (and, on first access, possible locking). - `lateinit` is a direct field access with a generated null check on read — essentially free after assignment. ## Gotcha `lateinit` gives no compile-time guarantee it was assigned; forgetting leads to a runtime exception. `lazy` can't be 'forgotten' because the block defines the value, but a `lazy` block that itself reads other not-yet-ready state can still misbehave.

  • Why can't `lateinit` be used on an `Int` or a nullable type?
    `lateinit` uses the null reference as the 'uninitialized' sentinel. Primitives can't be null, and a nullable type's legitimate null would be indistinguishable from 'uninitialized'.
  • Is `by lazy` thread-safe and is `lateinit`?
    `by lazy` is thread-safe by default (SYNCHRONIZED). `lateinit var` provides no thread-safety; concurrent assignment/read must be coordinated by you.

saying these in an interview costs you the question

  • Says lateinit caches or auto-computes its value
  • Claims lazy can be used on a var
  • Thinks lateinit works on Int/Boolean or nullable types
  • Says reading an unset lateinit returns null
  • Treats them as interchangeable

context