skip to content

Compare the practical workarounds for non-smart-castable properties (local `val`, `?.let`, `!!`, `requireNotNull`) and explain the tradeoffs a code reviewer should weigh.

level: seniorimportance: nice to knowfreq 25%

answer

  1. All solve 'non-stable read' by snapshotting once
  2. Local val = default: readable, names value, single getter call
  3. requireNotNull/checkNotNull return value + message; beat !!
  4. !! = last resort: bare NPE, hides bugs, re-reads
  5. Watch side-effecting getters: snapshot reads once, not twice

basics

~20 s

Copy the value into a local val to keep it readable and safe, or use ?.let for a quick block. Use requireNotNull/checkNotNull when null is a real error. Avoid !! because it just throws on null and hides bugs.

solid answer

~50 s

All four address the same problem: a property read isn't stable, so you snapshot it. (1) **Local `val`**: `val v = obj.prop; if (v != null) ...` — clearest, gives the value a name, and the snapshot also avoids re-reading a costly or side-effecting getter. (2) **`?.let { }`**: concise for a single expression, but nesting hurts readability and `it` shadows poorly. (3) **`?: return` / Elvis**: great for early-exit guards. (4) **`requireNotNull(x)` / `checkNotNull(x)`**: return the non-null value *and* throw with a message when the contract is violated — preferred over `!!` when null is genuinely illegal, because they document intent and message. **`!!`** should be a last resort: it throws a bare NPE, can mask a real reassignment/getter bug, and gives no message. A reviewer should also note that snapshotting changes semantics for getters with side effects (you read once instead of twice).

code

kotlin · 6 lines
kotlin
// Guard-clause style with a message-carrying check
fun send(msg: Message) {
    val body = msg.body ?: return            // skip empty
    val token = requireNotNull(session.token) { "login first" }
    transport.post(token, body)              // both smart/typed as non-null
}

go deeper

for a junior

Can apply local val and ?.let and knows !! is risky.

for a middle

Chooses among the workarounds by context and knows requireNotNull vs checkNotNull semantics.

for a senior

Weighs readability, side-effecting getters, and concurrency, and reviews !! usage critically.

for a principal

Sets team conventions: when message-carrying guards are mandated, how to treat side-effecting getters, and how snapshotting interacts with thread-safety and API contracts.

## The shared idea: snapshot into a stable value Every non-smart-castable case (`var`, custom getter, `open`/`abstract`, delegated, cross-module) is solved by reading the value once into something the compiler trusts. ## Option 1 — local `val` (default choice) ```kotlin val v = obj.prop if (v != null) { use(v) // smart cast on the stable local } ``` Pros: readable, names the value, single getter invocation (avoids double side effects), works for arbitrarily complex branches. This is the idiomatic default. ## Option 2 — `?.let { }` ```kotlin obj.prop?.let { v -> use(v) } ``` Pros: concise, no explicit null check, scopes the non-null value. Cons: nested `let`s become unreadable; the lambda's `it`/return semantics can surprise (a bare `return` inside returns from the enclosing function only with a label). Best for one short operation. ## Option 3 — Elvis early return / default ```kotlin val v = obj.prop ?: return // guard clause val w = obj.prop ?: defaultValue // fallback ``` Pros: flattens nesting with guard clauses; expresses 'null is not interesting here'. The `?: return` form is a very common idiom. ## Option 4 — `requireNotNull` / `checkNotNull` ```kotlin val v = requireNotNull(obj.prop) { "prop must be set" } ``` `requireNotNull` throws `IllegalArgumentException` (validate arguments); `checkNotNull` throws `IllegalStateException` (validate state). Both **return** the non-null value, so you can assign it. Prefer these over `!!` when null indicates a programming error, because they carry a message and communicate intent. ## Why `!!` is the last resort ```kotlin use(obj.prop!!) // throws bare NPE if null ``` The **not-null assertion** `!!` throws a `NullPointerException` with no message, re-reads the property (so it can NPE even right after a successful check if the getter changed), and signals 'I gave up on the type system.' It hides exactly the bugs smart-cast limits were protecting against. Acceptable only in tests or where non-null is locally and obviously guaranteed. ## Reviewer checklist - Does the workaround **snapshot once**? Re-reading a getter with side effects (logging, lazy init, network) changes behavior. - Is `!!` used where a `requireNotNull`/`?:` would be clearer and safer? - Does a chain of `?.let` reduce readability versus a guard clause? - For a `var`, is there a real concurrency concern (could another thread null it between check and use)? The local-val snapshot also closes that race for the read. ## Terms - `?.` safe call, `?:` Elvis, `!!` not-null assertion, `let` scope function. - `requireNotNull`/`checkNotNull` — stdlib guards returning the non-null value or throwing. - **Snapshot** — capturing a value once into an immutable local to make reads stable.

  • When is `!!` actually acceptable?
    In tests, or when non-null is locally and obviously guaranteed and a richer guard would add noise. Even then, `requireNotNull` with a message is usually better in production code because it documents the invariant.
  • Does snapshotting a side-effecting getter change behavior?
    Yes. The original double-read pattern would invoke the getter twice; capturing into a local val invokes it once. For getters that log, lazily initialize, or hit the network, that's a semantic change a reviewer should notice.

Snapshotting a property is like screenshotting a live dashboard before you act on it — you reason about a frozen frame, not a moving one.

saying these in an interview costs you the question

  • Defaulting to `!!` for every non-smart-castable read
  • Deeply nested `?.let` chains that obscure control flow
  • Not realizing requireNotNull/checkNotNull return the value
  • Ignoring that re-reading a getter has side effects / a race
  • Using checkNotNull for argument validation (it's for state)

context