Null Safety
|
This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Null safety is arguably Kotlin’s best-known feature: nullability is part of the type, checked by the compiler
at compile time, rather than discovered as a NullPointerException at run time.
Nullable Types
Every type is non-nullable by default; appending ? makes it nullable, and the compiler then requires a null
check before most operations on it:
var name: String = "Ada"
// name = null // compile error: String is non-nullable
var maybeName: String? = "Ada"
maybeName = null // fine -- String? explicitly allows null
// println(maybeName.length) // compile error: maybeName might be null
Safe Call (?.)
?. calls a member only if the receiver is non-null, and evaluates to null otherwise — avoiding an explicit
if (x != null) guard for a single call, and chaining cleanly across several nullable steps:
val length: Int? = maybeName?.length // null if maybeName is null, else its length
val city: String? = user?.address?.city // chains through several nullable links safely;
// short-circuits to null at the first null link
Elvis Operator (?:)
?: supplies a default when the left side is null — Kotlin’s concise stand-in for a ternary
x != null ? x : default:
val length: Int = maybeName?.length ?: 0 // 0 if maybeName is null
fun requireUser(id: Int): User =
findUser(id) ?: throw NoSuchElementException("no user $id") // "?:" combined with "throw"
Because throw is itself an expression of type Nothing (see
Basic Types and Variables), the second example
type-checks: the Elvis operator’s result type is User regardless, since the right side either produces a
User or never returns at all.
Not-Null Assertion (!!)
!! forces a nullable expression to non-null, throwing NullPointerException immediately if it actually is
null. It exists as an escape hatch, but its use is a deliberate statement that null there would be a bug, not
a normal condition to handle — reach for ?./?: first in ordinary code:
val length = maybeName!!.length // throws NPE right here if maybeName is null
Safe Cast (as?)
as? attempts a cast and evaluates to null on failure, instead of throwing ClassCastException the way a
plain as cast would:
val value: Any = "hello"
val asInt: Int? = value as? Int // null -- value is not an Int, no exception thrown
val asString: String? = value as? String // "hello"
Platform Types from Java Interop
A type coming from Java code (which has no compile-time nullability information — see
Kotlin and the JVM) is represented as a platform type,
notated String! in error messages — Kotlin relaxes its usual compile-time null checks for it and trusts the
caller to know whether it can actually be null (informed, ideally, by a @Nullable/@NonNull annotation on the
Java side, which Kotlin does respect when present):
// java.util.Map<K, V>.get(key) is a platform type: Kotlin can't prove it's non-null
val value = javaMap.get("key") // typed as "V!" -- treat it as nullable to stay safe
Treating an unannotated platform type as non-null when it can genuinely be null is one of the few ways a
NullPointerException can still surface in Kotlin code — almost always at a Java interop boundary.
Smart Casts
Once the compiler has proven a value is non-null (or of a narrower type) along a particular code path, it automatically treats it that way for the rest of that path — no explicit cast needed:
fun printLength(text: String?) {
if (text != null) {
println(text.length) // smart-cast to non-null String inside this branch -- no "!!" needed
}
}
fun describe(value: Any) {
if (value is String) {
println(value.uppercase()) // smart-cast to String -- see Control Flow's "is" branches
}
}
A smart cast requires the compiler to be certain the value cannot change between the check and the use — it
does not apply to a mutable var property that another thread (or another part of the same class) could
reassign in between, in which case a local val copy is the usual fix.
See Also
-
Basic Types and Variables — the
Nothingtype behind `throw’s use in an Elvis-operator expression. -
Control Flow — smart casts inside
if/whenbranches in more detail. -
Kotlin and the JVM — platform types at the Java interop boundary.