skip to content

Compare `lateinit var` with `by lazy`. When would you choose each?

level: seniorimportance: should knowfreq 65%

answer

  1. lateinit = var, external assign; lazy = val, self-compute
  2. lazy allows primitives & nullables; lateinit doesn't
  3. lazy is thread-safe by default (SYNCHRONIZED)
  4. lateinit throws early; lazy initializes on first read
  5. DI/lifecycle -> lateinit; expensive cached value -> lazy

basics

~20 s

lateinit var is a mutable value you assign yourself from outside, later. by lazy is an immutable val that computes itself automatically the first time it's read. Use lateinit when something else supplies the value; use lazy when the object can compute it on demand.

solid answer

~40 s

`lateinit var` and `val ... by lazy` both defer initialization but differ fundamentally. `lateinit`: a `var`, externally assigned, non-null non-primitive only, throws `UninitializedPropertyAccessException` if read before set, supports reassignment and `::prop.isInitialized`. `lazy`: a `val`, self-initializing via a lambda on first access, allows nullable and primitive types, thread-safe by default (`LazyThreadSafetyMode.SYNCHRONIZED`, configurable to `PUBLICATION` or `NONE`), never throws for being 'unset' because the first read triggers computation, and caches the result. Choose `lazy` when the property can compute its own value and you want it once and immutable (expensive objects, caching). Choose `lateinit` when an external party must inject the value (DI frameworks, Android `onCreate`, test setup) and it isn't computable internally. `lateinit` is mutable and externally driven; `lazy` is immutable and self-driven.

code

kotlin · 10 lines
kotlin
class Module {
    // externally injected -> lateinit
    lateinit var logger: Logger

    // computed once, immutable, thread-safe -> lazy
    val connection: Connection by lazy { openConnection() }
}

// choose NONE mode when no concurrency to skip locking:
val cheap: Int by lazy(LazyThreadSafetyMode.NONE) { compute() }

go deeper

for a junior

Knows lazy auto-computes on first read and lateinit is set manually later.

for a middle

Lists the var-vs-val, primitive, and exception differences accurately.

for a senior

Maps each to real scenarios (DI/lifecycle vs expensive cached compute) and knows lazy's thread-safety modes.

for a principal

Reasons about concurrency guarantees, mutability contracts, and chooses the initialization strategy that best models ownership of the value.

## Two ways to defer Both postpone giving a property its value, but the mechanism and contract diverge. ### `lateinit var` ```kotlin class Screen { lateinit var view: View // var fun onCreate(v: View) { view = v } // assigned externally } ``` - A **`var`** — mutable, reassignable. - **You** assign it from outside; the object doesn't compute it. - Non-null, **non-primitive** only. - Reading before assignment throws `UninitializedPropertyAccessException`. - Inspect state via `::view.isInitialized`. ### `by lazy` ```kotlin class Report { val data: List<Row> by lazy { // val loadExpensiveData() // runs once, on first read } } ``` - A **`val`** — immutable, single computed value. - **Self-initializing**: the lambda runs on the first read and the result is cached. - Allows **nullable and primitive** types. - **Thread-safe by default** (`LazyThreadSafetyMode.SYNCHRONIZED`); you can pick `PUBLICATION` (lambda may run multiple times, first result wins) or `NONE` (no locking, single-thread only) via `lazy(mode) { ... }`. - Never throws for 'not initialized' — the read *is* the initialization. ## Side-by-side | | `lateinit var` | `val by lazy` | |--|----------------|----------------| | Mutability | `var` (reassignable) | `val` (one value) | | Who initializes | external code | the lambda, on first read | | Primitives | no | yes | | Nullable | no | yes | | Early-access behavior | throws | computes then returns | | Thread safety | none built in | SYNCHRONIZED by default | | State check | `::p.isInitialized` | `(delegate as Lazy).isInitialized()` | ## Decision guide - The value is **provided by someone else** (framework, DI, test harness) and not computable internally → **`lateinit`**. - The object can **compute** the value itself, you want it **once, immutable, cached**, possibly **expensive** → **`lazy`**. - You need to **reassign** over time → `lateinit` (lazy is a `val`). - You need a **primitive or nullable** deferred value → `lazy` (or `Delegates.notNull()` for a settable primitive). ## Gotcha: thread safety Multiple threads can race to assign a `lateinit var`; there's no synchronization. `lazy` (default mode) guarantees the initializer runs at most once even under concurrency.

  • Why can `lazy` hold a primitive but `lateinit` cannot?
    `lazy` stores its state in a delegate object that tracks initialization separately, so the underlying value can be a boxed primitive or even null. `lateinit` relies on a null sentinel in the backing field, which primitives can't represent.
  • Is `lateinit var` thread-safe for concurrent assignment?
    No — there's no built-in synchronization. If multiple threads may assign or first-read it, you must coordinate yourself; `lazy` (SYNCHRONIZED) handles this for you.

saying these in an interview costs you the question

  • Saying lazy is a var or can be reassigned
  • Claiming lateinit is thread-safe by default
  • Thinking lazy can be initialized from outside the lambda
  • Not knowing lazy supports primitives/nullables but lateinit doesn't
  • Believing lateinit caches/computes a value like lazy does

context