iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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)

Test yourself

Q1. A nullable type is written as…
Q2. Safe call operator is…
Q3. Elvis operator is…

Discussion

Loading…