06 · Null Safety Basics¶
Kotlin's type system distinguishes nullable from non-nullable types at
compile time, which eliminates most NullPointerExceptions before the
program even runs. This is one of Kotlin's most-loved features coming from
Java — get comfortable with ?, !!, ?:, and safe calls now, since
they're everywhere in idiomatic code.
Nullable vs. non-nullable types¶
By default, a Kotlin type cannot hold null. To allow it, add ? after the
type.
fun main() {
var name: String = "Alice"
// name = null // compile error -- String can't be null
var nickname: String? = "Al" // String? -- nullable
nickname = null // fine
println(nickname)
}
The compiler tracks this distinction everywhere: a String parameter is
guaranteed non-null by the time your function body runs, so you never need
defensive null checks for it.
The safe call operator ?.¶
Calling a method or property directly on a nullable type is a compile error.
The safe call operator ?. calls it only if the receiver isn't null,
producing null otherwise.
fun main() {
val nickname: String? = null
// println(nickname.length) // compile error -- nickname might be null
println(nickname?.length) // null -- safe call short-circuits to null
val realNickname: String? = "Al"
println(realNickname?.length) // 2
}
Safe calls chain naturally, short-circuiting to null at the first null
link:
class Address(val city: String?)
class Person(val address: Address?)
fun main() {
val person1: Person? = Person(Address("Boston"))
val person2: Person? = Person(null)
val person3: Person? = null
println(person1?.address?.city) // Boston
println(person2?.address?.city) // null
println(person3?.address?.city) // null
}
The Elvis operator ?:¶
?: supplies a default value when the expression on its left is null —
named for how the operator looks like a smiley face when rotated.
fun main() {
val nickname: String? = null
val displayName = nickname ?: "Anonymous"
println(displayName) // Anonymous
val length = nickname?.length ?: 0
println(length) // 0 -- combines safe call + default in one line
}
?: can also be used to return or throw early, which is a very common
Kotlin idiom for guard clauses:
fun printUpper(text: String?) {
val nonNull = text ?: return // exit the function early if text is null
println(nonNull.uppercase())
}
fun main() {
printUpper("hello") // HELLO
printUpper(null) // prints nothing, function returns immediately
}
The not-null assertion !!¶
!! converts a nullable type to non-null by force — if the value actually
is null, it throws a NullPointerException immediately. Use it sparingly,
only when you are certain (from context the compiler can't see) that the
value cannot be null.
fun main() {
val nickname: String? = "Al"
val length: Int = nickname!!.length // "I know this isn't null"
println(length)
val missing: String? = null
// println(missing!!.length) // throws NullPointerException at runtime
}
Avoid !! where possible
Reaching for !! defeats the purpose of Kotlin's null safety — it's a
promise to the compiler that can still blow up at runtime. Prefer ?.,
?:, or an explicit if (x != null) check instead. !! is best reserved
for truly exceptional situations (e.g. a bug you want to crash loudly
on) rather than routine null handling.
Smart casts with explicit null checks¶
Once the compiler proves a nullable variable isn't null (e.g. inside an
if block), it lets you use it as if it were non-null without ?. or !!.
fun describe(nickname: String?) {
if (nickname != null) {
// Inside this block, nickname is smart-cast to String (non-null)
println("Nickname has ${nickname.length} characters")
} else {
println("No nickname set")
}
}
fun main() {
describe("Al")
describe(null)
}
Safe casts with as?¶
as throws a ClassCastException on a bad cast; as? returns null
instead.
fun main() {
val obj: Any = "hello"
val asString: String? = obj as? String // "hello" -- succeeds
val asInt: Int? = obj as? Int // null -- fails safely, no exception
println(asString)
println(asInt)
}
How It Actually Works¶
Null safety is almost entirely a compile-time feature — the JVM bytecode
target has no idea what a nullable type is; String and String? erase to
the exact same JVM type, java.lang.String. What Kotlin actually does is run
a static "nullability analysis" during compilation that tracks, at every
point in your code, whether an expression's flow-derived null-status is
known-non-null, known-null, or unknown — and refuses to compile a dereference
unless the compiler can prove the receiver is non-null at that point. This is
why nickname.length is a compile error but assigning realNickname and
calling .length right after works: it's smart-cast, not a runtime check.
The safe-call operator ?. does produce real bytecode, though: nickname
?.length compiles to roughly if (nickname != null) nickname.length else
null — a genuine ifnull bytecode check inserted by the compiler, wrapping
the call. Chained safe calls (person1?.address?.city) become nested
null-checks, short-circuiting on the first null exactly like the equivalent
hand-written Java would. The Elvis operator ?: is the same idea from the
other direction — nickname ?: "Anonymous" compiles to a temporary variable,
a null check, and a conditional expression, no different from writing val
tmp = nickname; if (tmp != null) tmp else "Anonymous".
The !! operator (not shown above but coming later) is the one place
nullability becomes a genuine runtime check: it compiles to an explicit
Intrinsics.checkNotNull(value) call injected by the compiler, which throws
KotlinNullPointerException immediately if the value is null — meaning !!
is really just Kotlin choosing to fail loudly and immediately at the
assertion site rather than let a stray null propagate and blow up somewhere
less obvious later, which is exactly what Java's implicit NPEs do. In fact
the compiler inserts these same Intrinsics.checkNotNullParameter calls at
the top of every public function for non-null parameters, so a Java caller
passing null into a Kotlin function still gets a fast, clear exception
instead of silently violating Kotlin's non-null guarantee deep inside the
function body.
Cheat sheet¶
| Operator | Purpose | Example |
|---|---|---|
? |
Declare a nullable type | var x: String? = null |
?. |
Safe call — no-op if receiver is null | x?.length |
?: |
Elvis — default if left side is null | x?.length ?: 0 |
?: + return/throw |
Guard clause | val v = x ?: return |
!! |
Force non-null, throws if actually null | x!!.length |
as? |
Safe cast — null instead of exception | obj as? String |
| smart cast | Compiler narrows type after a null check | if (x != null) x.length |
🔀 See this in another language¶
Exercise¶
Write a function safeDivide(a: Int, b: Int): Int? that returns null if
b is zero, otherwise the integer division result. Then write a function
describeDivision(a: Int, b: Int): String that calls safeDivide and uses
the Elvis operator to produce "Result: N" on success or
"Cannot divide by zero" on null. Test it with a few values including
b = 0.