Null Safety
Kotlin removes NullPointerException not with runtime checks but with the type system: String and String? are two different types, and the compiler simply will not let a value of the second one through where the first is expected. No wrapper is allocated to do this: in bytecode a String? is the very same reference to java.lang.String, and the whole difference lives at compile time. Two consequences follow at once. First, ? is not a lint hint or a flag on the variable — it is part of the type, so the check cannot be "turned off", only satisfied. Second, nullability is free for reference types, but an Int? must be able to hold null, which 32 bits of a primitive cannot — so it is always boxed into an Integer (the details are in Boxing & Equality).
The tools of the topic split along exactly that line. ?. and ?: handle absence: the safe call short-circuits the expression to null, Elvis supplies a fallback or an early exit. A smart cast proves the absence of absence — and it works only where the compiler can prove the value did not change between the check and the use (on a var property of a class, on a mutable property of another object, and on a property with a custom getter it honestly refuses). !! proves nothing and converts nothing — it is an assertion, and it throws NullPointerException if you were wrong. And lateinit takes the field out from under the initialization check altogether, trading it for an UninitializedPropertyAccessException on an early read.
⚠️ And here is the topic's honest hole. Values arriving from Java carry a platform type (T!): unannotated Java code carries no nullability information, the compiler does not know what it is looking at, and so it suspends the check — it accepts the value as String and as String? alike. There is no check at the call site; null slips quietly into a non-null type and blows up later, at the first use, often in a completely different file. It is closed with @Nullable/@NotNull annotations on the Java side, or by declaring the type nullable yourself at the boundary — see Java interop. The layer-by-layer breakdown is below.
Topic map
- Nullable types — why
StringandString?are different types, what that costs at runtime, how it differs fromOptional, and where platform typesT!come from. - The safe call and the Elvis operator — how
?.short-circuits a chain tonull, why the right side of?:is evaluated lazily, and how to turn Elvis into an early exit. - The not-null assertion —
!!as an assertion rather than a conversion, where it is justified, and why it must never be used to silence the compiler. - Smart casts — what exactly the compiler has to prove, the three cases where it refuses, and how a local
valfixes it. - lateinit — a property with no initializer but a non-null type,
UninitializedPropertyAccessExceptioninstead of an NPE, and how it differs fromby lazy. - The Nothing type — the bottom type with no values at all, why
throwis an expression, and howNothingmakes?: throwlegal.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Taking ? for a lint hint rather than part of the type | It is a compile error, not a warning: a String? will not go where a String is expected |
Treating String and String? as one type carrying a flag | They are different types and cannot be swapped — yet at runtime the representation is identical |
Thinking String? compiles into a wrapper like Optional | There is no wrapper: the same reference, the whole difference is at compile time. And a reference to an Optional can itself be null |
| Expecting the compiler to treat an unannotated Java type as nullable | It hands you a platform type T! and demands no check — null slips into a non-null type in silence |
| Trusting a Java getter's non-null type without checking at the boundary | The NPE surfaces far from the Java call — at the first use, often in someone else's file |
Believing ?. catches an exception | ?. catches nothing: it simply does not invoke the member and yields null at once |
Expecting the right side of ?: to be evaluated eagerly | It runs only when the left side is null — an expensive fallback never fires for nothing |
Thinking the rest of a?.b?.c still runs after a broken link | The chain stops at the first null; the whole expression is null immediately |
Calling !! a conversion from T? to T | It is an assertion: on null it throws NullPointerException right there |
Adding !! just to "agree" with the compiler | You did not remove the null, you moved the crash to runtime — and lost the compiler's warning |
Thinking a smart cast works on any var, a class property included | A mutable property is not provably stable — the compiler refuses even with the null check right beside it |
| Believing a property with a custom getter can be smart-cast | Every getter call may return a new value, so the narrowing is forbidden |
Reaching for !! instead of copying the value into a local val | The race turns into an NPE instead of going away |
Putting lateinit on a val or on a primitive Int | It does not compile: lateinit is allowed only on a non-null var of a reference type |
Expecting an early read of a lateinit to yield null | It throws UninitializedPropertyAccessException — which is not an NPE |
Confusing lateinit with by lazy | by lazy is a val that computes itself on first read; lateinit waits for you to assign the value from outside |
Confusing Nothing with Unit | Unit has exactly one value; Nothing has none, and an expression of that type never completes normally |
Calling Nothing a supertype of every type | The other way round: it is a subtype of every type, which is why ?: throw fits where a value is expected |
Interview relevance
Null safety comes up in almost every Kotlin interview, and not for the syntax — everybody knows ?.. What is probed is the boundary of the guarantee: do you understand that the compiler promises exactly what it can prove, and can you name the places where the proof is missing? A candidate who answers "where do NPEs still happen in Kotlin?" with "only where I write !! myself" has already lost: they do not see platform types and will trust someone else's Java API. A candidate who opens with "String and String? are different types, and out of Java you get a T! whose check is suspended" instantly reads as someone who has worked on real code.
Typical checks:
- What exactly
?marks, and at what moment it is checked. - How
?.differs fromtry/catch, and why?:is lazy on the right. - What
!!really does, and where it is justified. - The condition for a smart cast, and the three cases where the compiler refuses.
- The difference between
lateinitandby lazy, and which exception an early read throws. - What
Nothingis, why it is a subtype of every type, and how that relates to?: throw. - Where the claim "Kotlin has no NPEs" breaks down — and how to close the Java boundary.
Common wrong answer: "the compiler treats an unannotated Java type as nullable and demands a check." It is precisely the other way round: it hands you a platform type T! for which the check is suspended — you are free to assign it to a String and to a String? alike, and the compiler stays quiet. That is exactly why such an NPE surfaces not on the line that called Java but later, at the first access to the value — and candidates confident in the "zero NPE" model go looking for the cause in the wrong place. The second classic failure is calling !! a conversion: !! converts nothing, it throws NullPointerException, and its one legitimate role is to make the failure loud and local where you can prove non-null and the compiler cannot.