When would you choose `by lazy` over `lateinit var`, and how do their initialization and threading semantics differ?
answer
- lazy = val, self-computed, cached, thread-safe default
- lateinit = var, you assign it, non-null, no caching
- lateinit forbids nullable & primitive types
- lateinit unset read -> UninitializedPropertyAccessException
- Check with ::prop.isInitialized (lateinit)
basics
~10 sUse 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 linesclass 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
Knows lazy is a self-computing val and lateinit is a var you assign later.
Articulates val-vs-var, caching, thread-safety differences, and lateinit's type restrictions and exception.
Maps each to concrete scenarios (DI/test setup vs. expensive read-only init) and discusses per-access cost and isInitialized.
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