When is `lateinit` + `isInitialized` the right tool versus a nullable property or `by lazy`?
answer
- Nullable = absence is a real value
- lateinit+isInitialized = non-null, set later, guard the window
- by lazy = computed once on first read, no 'unset' state
- isInitialized is declaring-class only
- Heavy isInitialized use = consider lazy/nullable
basics
~20 sUse lateinit+isInitialized when a non-null value is set later by a framework and you sometimes need to ask 'is it set yet?'. Use nullable when absence is a normal value. Use by lazy when the value is computed once on first use.
solid answer
~50 sThree patterns for 'value not available at construction': - **Nullable (`var x: T? = null`)** — when *absent* is a legitimate domain state; callers use `?.`/`?:`. Most honest and works across class boundaries. - **`lateinit var` + `isInitialized`** — when the value is genuinely non-null but assigned by a lifecycle hook or DI after construction, and you'd rather not pay `?.`/`!!` on every read. `isInitialized` exists to guard the narrow window where it might be unset. Restricted to the declaring class. - **`by lazy { }`** — when the value is computed exactly once on first access and there is no observable 'not yet' state you care about; thread-safe by default (`LazyThreadSafetyMode.SYNCHRONIZED`). Reaching for `isInitialized` often signals that a method can run before initialization — sometimes the real fix is `lazy` (removes the unset window) or nullable (makes absence explicit). Use `isInitialized` when the unset window is real and unavoidable (Android views, test fixtures, framework callbacks).
go deeper
Knows the three options exist and roughly when each fits.
Contrasts trade-offs: nullable noise vs lateinit's unset window vs lazy's no-choice timing.
Identifies frequent isInitialized use as a design smell and steers to lazy/nullable; notes lateinit's type constraints and scope limit.
Connects the choice to API stability and lifecycle modeling — designs so the unset window doesn't leak to callers in the first place.
## The shared problem All three handle 'I can't set this in the constructor.' They differ in what 'not set' *means* and how it's exposed. ## Nullable property ```kotlin var listener: Listener? = null ``` - 'Not set' is a first-class value (`null`); callers handle it with `?.`, `?:`, `!= null`. - Crosses class boundaries: anyone can check `obj.listener != null`. - Cost: nullable noise everywhere even after you 'know' it's set. Choose when absence is a normal, lasting domain state. ## `lateinit var` + `isInitialized` ```kotlin lateinit var binding: Binding fun show() { if (this::binding.isInitialized) binding.render() } ``` - Type stays **non-null** after assignment — no `?.`/`!!` on normal reads. - Reading before assignment throws `UninitializedPropertyAccessException`; `isInitialized` is the guard for the (hopefully brief) unset window. - **Declaring-class only**; not exposable directly to callers. - Cannot be a `val`, primitive, or nullable type. Choose when the value is set once by a framework/lifecycle and is logically non-null afterward, and you occasionally need to test readiness inside the class. ## `by lazy { }` ```kotlin val config by lazy { loadConfig() } ``` - Computed on first access, cached; no externally observable 'unset' state. - Thread-safe by default; modes `SYNCHRONIZED` / `PUBLICATION` / `NONE`. - You don't choose *when* it initializes — first read does. Choose when the value is derivable on demand and you never need to ask 'has it been set yet?'. ## Decision guide - Need to observe 'not set yet' from inside the class, value injected externally, must be non-null → **lateinit + isInitialized**. - 'Absent' is a real, possibly permanent value, or callers outside need to know → **nullable**. - Value computes itself once on demand → **by lazy** (and `isInitialized` becomes unnecessary). ## Common anti-pattern Using `isInitialized` as flow control on every read is a smell — if the value is computed on demand, `lazy` is cleaner; if absence is meaningful, nullable is clearer.
- If you switch a property to by lazy, do you still need isInitialized?No — lazy has no observable 'unset' state; first access initializes it, so the check becomes meaningless.
- Why not just make every late value nullable?It forces ?./!! on every read even after the value is known set; lateinit keeps the type non-null for the common case.
saying these in an interview costs you the question
- Recommending isInitialized when by lazy would remove the unset window entirely
- Claiming lateinit works on nullable or primitive types
- Saying nullable and lateinit are interchangeable with no trade-offs
- Using isInitialized as routine flow control on every access