Compare the Elvis operator with alternatives like ?.let, requireNotNull/checkNotNull, and !! for handling nullable values. When would you choose each?
answer
- ?: = default or exit
- requireNotNull/checkNotNull = assert + return value
- checkNotNull throws IllegalState; requireNotNull throws IllegalArgument
- !! = bare NPE, last resort
- ?.let = transform when present, composes with ?:
basics
~20 sUse ?: to supply a default or to exit early on null. Use requireNotNull/checkNotNull to assert a value must exist with a clear message. Avoid !!, which throws an unhelpful NPE. Use ?.let to run code only when non-null.
solid answer
~40 s`?:` is for **producing a value or exiting**: `x ?: default` (substitute) or `x ?: return/throw` (guard). `requireNotNull(x)`/`checkNotNull(x)` **return** the non-null value or throw `IllegalArgumentException`/`IllegalStateException` with a lazily-built message — ideal for precondition/invariant checks where you want the value, not a default. `!!` throws a bare `NullPointerException` and should be reserved for cases the compiler can't prove but you can — overuse is a code smell. `?.let { }` runs a block **only when non-null**, returning the block's result (or null), which differs from Elvis: Elvis chooses between values, `let` transforms the present value. They compose: `x?.let { transform(it) } ?: fallback`. Choose Elvis for defaults/guards, requireNotNull for asserting-with-message, let for conditional transformation, and `!!` rarely.
code
kotlin · 6 linesfun resolve(raw: String?, cfg: Config?): String {
// assert with message, get value
val c = requireNotNull(cfg) { "config required" }
// transform when present, else default
return raw?.let { it.trim() }?.ifEmpty { null } ?: c.defaultName
}go deeper
Knows ?: gives a default and that !! can crash; basic awareness of alternatives.
Picks ?: vs requireNotNull vs !! correctly for default vs assert vs proven-non-null.
Distinguishes require vs check exception semantics, composes ?.let ?:, and explains the let-returns-null pitfall.
Establishes team policy banning casual !!, standardizing assertion helpers, and consistent error types across modules.
## The toolbox for nullable values ### Elvis `?:` — substitute or exit ```kotlin val port = configPort ?: 8080 // default val user = repo.find(id) ?: return null // guard / early exit val u2 = repo.find(id) ?: throw NotFound(id) ``` Use when you want **a concrete fallback value** or a **fail-fast early exit** inline. ### requireNotNull / checkNotNull — assert and get the value Both are stdlib functions that **return the non-null value** or throw: - `requireNotNull(x) { "msg" }` → throws `IllegalArgumentException` (for **argument** validation). - `checkNotNull(x) { "msg" }` → throws `IllegalStateException` (for **state/invariant** validation). The message lambda is **only evaluated on failure** (lazy). Prefer these over `x ?: throw ...` when the thrown type and semantics match, because they read as intent ("this must not be null") and standardize messages. ```kotlin fun start(cfg: Config?) { val c = requireNotNull(cfg) { "config must be provided" } // c: Config (non-null) } ``` ### Not-null assertion !! — last resort `x!!` returns `x` if non-null else throws `NullPointerException` with little context. It defeats null safety and should appear only when you have proof the compiler lacks (e.g., after external validation it can't track). Teams often lint against `!!`. Prefer `?:`, `requireNotNull`, or smart-casting guards instead. ### ?.let — conditional transformation `?.let { block }` invokes `block` with the non-null value as `it` **only when the receiver is non-null**, returning the block's result, else `null`. This is **not** the same as Elvis: `let` is about doing/transforming when present; Elvis is about choosing a value. They combine: ```kotlin val label = rawName?.let { it.trim().uppercase() } ?: "UNKNOWN" ``` ## Decision guide | Need | Use | |------|-----| | Provide a default value | `x ?: default` | | Early-exit on null | `x ?: return` / `x ?: throw` | | Assert non-null, keep value, clear message | `requireNotNull` / `checkNotNull` | | Transform only when present | `x?.let { ... }` (often with `?:`) | | You can prove non-null, compiler can't | `!!` (sparingly) | ## A subtle pitfall: let returning null If the `let` block can itself return `null` (or the value is a valid result that equals a falsy-looking default), `x?.let { ... } ?: fallback` may apply the fallback when the block returned `null`, not only when `x` was null. Be intentional about which null you're defaulting.
- Why prefer requireNotNull over `x ?: throw IllegalArgumentException(...)`?It expresses intent succinctly, returns the value, and standardizes the exception type and lazy message; the Elvis-throw form is fine when you need a custom exception type.
- How can `x?.let { } ?: d` surprise you?If the let block itself yields null, the Elvis fallback `d` is applied even though `x` was non-null.
- What does !! throw and why avoid it?A bare NullPointerException with poor context; it bypasses null-safety and hides where the real invariant should be checked.
saying these in an interview costs you the question
- Reaching for !! as the default tool
- Thinking ?.let and ?: are interchangeable
- Confusing requireNotNull (IllegalArgument) with checkNotNull (IllegalState)
- Believing the requireNotNull message is always evaluated
- Not seeing the let-returns-null edge case