Null Safety
Kotlin distinguishes nullable (T?) from non-null (T) at the type level. ?., ?:, !!, let — the toolkit that makes NPE practically impossible in idiomatic code.
Nullable types, safe calls, Elvis, let
EXAMPLE
// 1) Nullable vs non-null
var a: String = "hello"
// a = null // ERROR — type is non-null
var b: String? = "hello"
b = null // OK — type is nullable
// 2) Safe call ?. — short-circuit on null
val len: Int? = b?.length // null if b is null, else b.length
val city: String? = user?.address?.city
// 3) Elvis ?: — default for null
val len2: Int = b?.length ?: 0
val display = name ?: "Anonymous"
val token = config["api_key"] ?: error("missing api_key")
val value = someNullable() ?: return // early return
// 4) Non-null assertion !! — throw NPE if null
val forced: String = b!! // throws KotlinNullPointerException if b is null
// Use only when you're CERTAIN. Treat as a code-smell to be reviewed.
// 5) Smart casts — Kotlin tracks null checks
fun greet(name: String?) {
if (name != null) {
println("Hi, \${name.uppercase()}") // name is now String, not String?
}
}
// Works for type checks too
fun describe(obj: Any?) {
when (obj) {
is String -> println("string of length \${obj.length}")
is Int -> println("int \$obj")
null -> println("null")
else -> println("other: \$obj")
}
}
// 6) let — execute block only if not null
b?.let {
println("non-null: \${it}")
}
val name: String? = getName()
name?.let { fullName ->
save(fullName)
notify(fullName)
}
// 7) Common scope functions with null-safety
user?.let { sendEmail(it.email) } // do something if not null
user?.also { audit(it) } // side effect, return user
user?.run { validate(); save() } // execute methods on user
user?.apply { lastSeen = now } // mutate, return user
user?.takeIf { it.active } ?: return // filter
// 8) Lateinit — non-null but initialised later (Android-style)
class MainActivity {
lateinit var binding: ActivityMainBinding
fun onCreate() {
binding = ActivityMainBinding.inflate(layoutInflater)
// From here, binding is non-null
}
}
// Accessing before init throws UninitializedPropertyAccessException
if (::binding.isInitialized) { ... }
// 9) by lazy — initialise on first use, never null
class Config {
val api: String by lazy { fetchApi() }
}
// 10) Collections + nulls
val list: List<String> = listOf("a", "b", "c") // non-null list of non-null strings
val list2: List<String?> = listOf("a", null, "c") // non-null list, nullable elements
val list3: List<String>? = null // nullable list
// Filter nulls
list2.filterNotNull() // List<String>
// 11) Java interop — !! danger zone
// Methods from Java return 'platform types' — T! (could be null, could not).
// Kotlin treats them as nullable IF you choose; access them via:
val s: String? = javaApi.maybeNullString()
val s2: String = javaApi.maybeNullString() ?: ""
// Always check Java APIs at the boundary; use @@Nullable / @@NotNull in Java code if possible.
// 12) Functional + nullable
val lengths = names.map { it.length } // List<Int>
val lengthsNullable = names.map { it?.length } // List<Int?>
// 13) requireNotNull / checkNotNull — preconditions
fun process(name: String?) {
val n = requireNotNull(name) { "name is required" }
// n is now non-null String
}
// 14) Optional chaining for chains
fun userCity(id: Int): String? = repo.findUser(id)
?.address
?.city
?.lowercase()
// 15) Nullable receiver — function defined on T?
fun String?.orEmpty(): String = this ?: "" // built into stdlib
fun Int?.orZero(): Int = this ?: 0
val s: String? = null
println(s.orEmpty()) // ""
// 16) Tips
// • Default to non-null types; reach for nullable only when null carries meaning
// • Avoid !! — refactor to make non-null guarantees explicit
// • Use ?: for sensible defaults, not !! for 'I know it's not null'
// • Prefer ?.let { } over `if (x != null) { ... x ... }`
// • For Android, leverage view-binding + lateinit for late-init non-null views
// • Use Kotlin's @@NotNull annotations on JVM-facing APIs
Why it matters
Reach for ?. + ?: before !!. !! works but signals “I might be wrong” — track them in code review like raw SQL or any. Smart casts + scope functions make most nullable handling read as cleanly as non-null code.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
var name: String? = null
name = "Ada"
println(name?.length ?: 0) // safe call + Elvis
name?.let { println("got $it") }
Try it Yourself »
Exercise
Safe-call operator.
println(name
length)
Two characters.
Discussion
Loading…