At a Java boundary, what is the difference between guarding a platform value with requireNotNull/checkNotNull versus using the !! operator, and when should each be used?
answer
- !! -> bare NPE, no message
- requireNotNull -> IllegalArgumentException + message
- checkNotNull -> IllegalStateException + message
- All smart-cast to non-null
- Best option: ?: / ?. to avoid asserting
basics
~10 sAll of them crash if the value is null, but requireNotNull/checkNotNull let you attach a clear message and throw a meaningful exception, while !! throws a bare NullPointerException with no explanation.
solid answer
~40 s`!!` (the not-null assertion operator) converts `T?`/platform `T!` to `T` and throws a bare `NullPointerException` (actually `KotlinNullPointerException`) if the value is null — no message, no intent. `requireNotNull(x)` and `checkNotNull(x)` also assert non-null and smart-cast `x` to non-null, but throw `IllegalArgumentException` and `IllegalStateException` respectively, accept a lazily-evaluated message lambda, and communicate intent: `require` for validating inputs/arguments, `check` for validating internal state/invariants. At a Java boundary you generally want `requireNotNull` with a message ("name missing from repo") so the failure is self-documenting and located at the edge. Reserve `!!` for the rare case where null is genuinely impossible and a terse assertion suffices, or in throwaway code. Best of all is often avoiding the assertion entirely with `?:`/`?.`/`filterNotNull`.
code
kotlin · 3 linesval a = repo.findName()!! // bare NPE
val b = requireNotNull(repo.findName()) { "name missing" } // IAE + message
val c = repo.findName() ?: "anonymous" // no throwgo deeper
Knows !! crashes without a message and requireNotNull gives a clearer error.
Distinguishes the three exception types, the lazy message lambda, and that all smart-cast to non-null.
Chooses require vs check by intent and prefers ?:/?. when null is a valid case at the boundary.
Standardizes boundary guards (requireNotNull + message), bans casual !!, and ties it to diagnosability/observability.
## The three assertion tools All three take a possibly-null value (including a platform type) and either return it as non-null or throw. ### `!!` — the not-null assertion operator ```kotlin val name: String = repo.findName()!! ``` - Converts `String?` / `String!` to `String`. - If null, throws a bare `NullPointerException` (the Kotlin runtime type `KotlinNullPointerException`) with **no message**. - Terse but uninformative; a stack trace pointing at `!!` is all you get. ### `requireNotNull(value) { message }` ```kotlin val name = requireNotNull(repo.findName()) { "name missing for id=$id" } ``` - Returns the value **smart-cast to non-null** (`name: String`). - Throws **`IllegalArgumentException`** if null. - Message lambda is **evaluated lazily** (only on failure), so no cost on the happy path. - Semantics: *"this argument/input must not be null."* ### `checkNotNull(value) { message }` - Identical mechanics but throws **`IllegalStateException`**. - Semantics: *"this object/state invariant must hold."* ## Choosing at a Java boundary | Tool | Throws | Message | Use for | |------|--------|---------|---------| | `!!` | NPE (bare) | none | impossible-null, throwaway | | `requireNotNull` | IllegalArgumentException | yes (lazy) | validating incoming Java values/args | | `checkNotNull` | IllegalStateException | yes (lazy) | validating internal invariants | For a platform value crossing from Java, **`requireNotNull` with a message** is usually best: it documents the assumption, fails fast at the edge, and yields a diagnosable exception instead of a cryptic NPE deep in the call chain. Pair it with the choice of an explicit boundary type. ## Often, assert nothing The strongest option is to not assert at all and handle null: ```kotlin val name = repo.findName() ?: "anonymous" // Elvis default repo.findName()?.let { use(it) } // safe-call ``` Use an assertion only when null truly indicates a bug. ## Common confusion `requireNotNull`/`checkNotNull` are **not** the same as `require(x != null)` followed by `x!!` — they return the smart-cast value directly, so you keep a non-null binding without a second deref.
- Why is the message argument to requireNotNull a lambda rather than a plain String?So it is evaluated lazily — only when the check fails — avoiding string-building cost on the common non-null path.
- Does requireNotNull smart-cast the value, or do you still need !! afterward?It returns the value smart-cast to non-null, so `val x = requireNotNull(y)` gives a non-null `x` directly; no `!!` needed.
saying these in an interview costs you the question
- Saying `!!` throws an IllegalStateException
- Believing requireNotNull and checkNotNull throw the same exception type
- Sprinkling `!!` across a Java boundary as the default
- Not knowing the message lambda is lazily evaluated
- Thinking these don't smart-cast and you must `!!` again after