Compare the practical workarounds for non-smart-castable properties (local `val`, `?.let`, `!!`, `requireNotNull`) and explain the tradeoffs a code reviewer should weigh.
answer
- All solve 'non-stable read' by snapshotting once
- Local val = default: readable, names value, single getter call
- requireNotNull/checkNotNull return value + message; beat !!
- !! = last resort: bare NPE, hides bugs, re-reads
- Watch side-effecting getters: snapshot reads once, not twice
basics
~20 sCopy 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 sAll 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// 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
Can apply local val and ?.let and knows !! is risky.
Chooses among the workarounds by context and knows requireNotNull vs checkNotNull semantics.
Weighs readability, side-effecting getters, and concurrency, and reviews !! usage critically.
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)