When would you choose `lateinit var` versus `by lazy`, and what are the key differences?
answer
- lateinit = external sets it, var, no init lambda
- lazy = you compute it, val, init lambda, cached
- lazy allows primitives/nullable; lateinit doesn't
- lazy is SYNCHRONIZED by default; lateinit isn't
- Who provides the value decides
basics
~10 sUse by lazy for a val you compute yourself on first read. Use lateinit var for a non-null var that something else sets later, like dependency injection or test setup.
solid answer
~40 s`by lazy` is a delegated `val`: it runs an initializer lambda on first access, caches the result, and is read-only — *you* own the computation. `lateinit var` is a mutable `var` you (or a framework) assign externally; there is no initializer lambda and the value can be reassigned. Use `lateinit` when an external actor supplies the value after construction (DI fields, JUnit `@BeforeEach`, Android `onCreate`) and you can't compute it yourself. Use `lazy` when you *can* compute the value but want to defer the cost until first use. Differences: `lazy` works with `val`, nullable types, and primitives; `lateinit` requires non-null `var` and forbids primitives. `lazy` is thread-safe by default (`LazyThreadSafetyMode.SYNCHRONIZED`); `lateinit` has no synchronization. `lazy` can't be reset; `lateinit` can be reassigned and checked via `::prop.isInitialized`.
code
kotlin · 10 linesclass Screen {
// External: a DI framework assigns this after construction
lateinit var presenter: Presenter
// Self-computed, deferred + cached, thread-safe by default
val formatter: DateFormatter by lazy { DateFormatter(locale = currentLocale()) }
// lazy can be a primitive; lateinit cannot
val maxRetries: Int by lazy { config.readInt("retries") }
}go deeper
Knows lateinit is set later by something else and lazy computes on first use.
Contrasts var vs val and external-assignment vs self-computation, and knows lazy supports primitives.
Articulates the full difference table including thread-safety modes, isInitialized, and the 'who provides the value' decision rule.
Guides team conventions: prefer constructor injection where possible, reserve lateinit for framework-driven fields, choose lazy thread-safety modes deliberately.
## Two different mechanisms ### `lateinit var` - Mutable (`var`), assigned **externally**, no initializer. - Compiler inserts an uninitialized-access guard; reading before assignment throws `UninitializedPropertyAccessException`. - Restrictions: non-null only, no primitives, no `val`, no custom accessors. ### `by lazy` - A **property delegate** producing a read-only (`val`) value. - You supply a lambda; it runs **once on first access**, the result is cached, subsequent reads return the cache. - No restrictions on primitives/nullability; it's a `val`. ```kotlin class Example { lateinit var injected: Repository // set by DI later val config: Config by lazy { loadConfig() } // computed on first read } ``` ## Decision rule - **Who provides the value?** - *Something external* (framework, test setup, lifecycle callback) → `lateinit var`. - *You can compute it* but want to defer the cost → `by lazy`. - **Mutable or read-only?** Need reassignment → `lateinit var`. Compute-once → `lazy`. - **Primitive or nullable?** `lazy` supports both; `lateinit` supports neither. ## Key differences table | Aspect | `lateinit var` | `by lazy` | |---|---|---| | Mutability | `var` (reassignable) | `val` (read-only) | | Who initializes | external code/framework | the lazy lambda | | Initializer lambda | none | required | | Primitives allowed | no | yes | | Nullable type allowed | no | yes | | Thread safety | none | `SYNCHRONIZED` by default | | Check if set | `::prop.isInitialized` | not directly exposed | | Failure before set | `UninitializedPropertyAccessException` | n/a (computes on access) | ## Thread-safety nuance `by lazy` defaults to `LazyThreadSafetyMode.SYNCHRONIZED` — the initializer runs at most once even under concurrent first access. Other modes: `PUBLICATION` (initializer may run more than once, first result wins) and `NONE` (no locking, fastest, single-thread only). `lateinit` provides **no** synchronization; if multiple threads race to assign it, that's on you. ## Common mistakes - Using `lateinit` for something you could compute yourself (use `lazy`). - Using `lazy` for a value an external framework must inject (the lambda can't get it). - Forgetting `lazy` is a `val` — you can't reassign it.
- Is `by lazy` thread-safe?By default yes — `LazyThreadSafetyMode.SYNCHRONIZED` ensures the initializer runs once. You can opt down to `PUBLICATION` or `NONE` for performance when safe.
- Can you reset or reassign a `by lazy` value?No. It backs a `val` and caches the first computed result; there's no public reset. If you need reassignment, use `lateinit var` (or a plain nullable var).
saying these in an interview costs you the question
- Saying lazy and lateinit are interchangeable
- Claiming lateinit takes an initializer lambda
- Thinking lazy works on `var`
- Not knowing lazy is thread-safe by default
- Using lateinit where the value could be self-computed