Nullable Types and the Type System
The Billion-Dollar Mistake
Tony Hoare called null references his "billion-dollar mistake." NullPointerExceptions account for roughly 40% of all runtime errors in Java. Kotlin's type system addresses this directly by making nullability explicit at the type level.
Non-Nullable vs Nullable Types
By default, every type in Kotlin is non-nullable. You cannot assign null to it:
val name: String = "Kotlin"
// name = null // Compile error: Null can not be a value of a non-null type String
To allow null, append ? to the type:
val nullableName: String? = null // Allowed
val alsoValid: String? = "Kotlin" // Also allowed
This is a compile-time distinction. The JVM still uses null internally, but Kotlin's compiler prevents null from propagating through non-nullable types. You handle nulls explicitly where they can occur.
Why This Matters
In Java, every reference can be null. This means every method call, every field access, every array element access can throw a NullPointerException. Developers must mentally track which references might be null across entire call chains.
Kotlin forces you to address null at the point of assignment. If a variable is String?, the compiler requires you to handle the null case before using it as a String. This shifts null handling from runtime to compile time.
Null in Collections
Kotlin distinguishes between a collection of nullable elements and a nullable collection:
val nonNullList: List<String> = listOf("a", "b")
// nonNullList cannot contain null elements
val nullableList: List<String?> = listOf("a", null, "b")
// Elements can be null
val nullableListOfStrings: List<String>? = null
// The list itself can be null
Platform Types
When interoperating with Java code, Kotlin cannot determine nullability from Java annotations. It uses platform types (String!, Int!, etc.), which behave like nullable types for safety:
// Java method: public String getName() { ... }
val javaName: String = javaObject.name // Compiler trusts you
val safeName: String? = javaObject.name // Safer: null-checked
Always treat Java return values as nullable unless you can verify nullability through annotations (@Nullable, @NonNull).
Safe Calls, Let, and the Elvis Operator
The Safe-Call Operator (?.)
The safe-call operator chains property access or method calls that might return null. If any part of the chain is null, the entire expression returns null:
val name: String? = "Kotlin"
println(name?.length) // 6
val nullName: String? = null
println(nullName?.length) // null (no exception)
Without the safe-call operator, accessing .length on a nullable String would require an explicit null check.
Safe Calls with let
The let function scopes a non-null value to a block. Combined with ?.let, it executes code only when the value is non-null:
val name: String? = "Kotlin"
name?.let {
println("Name has ${it.length} characters")
// 'it' is the non-null String inside this block
}
// If name is null, this block is skipped entirely
This is the idiomatic way to perform null-conditional operations:
fun processUser(user: User?) {
user?.let {
println("Processing ${it.name}")
sendEmail(it.email)
logAccess(it.id)
}
// No else branch needed; null is silently ignored
}
The Elvis Operator (?:)
The Elvis operator provides a default value when the left side is null. It reads as "if not null, use this; otherwise, use that":
val name: String? = null
val displayName = name ?: "Anonymous"
println(displayName) // Anonymous
val length = name?.length ?: 0
println(length) // 0
The Elvis operator is essential when you need a concrete value (not nullable) from a nullable source:
fun getConfigValue(key: String): String {
return cache[key] ?: database[key] ?: defaultConfig[key] ?: ""
}
This chain tries multiple sources and falls back to an empty string.
Non-Null Assertions (!!)
The !! operator converts a nullable type to its non-null counterpart, throwing a NullPointerException if the value is null:
val name: String? = null
println(name!!.length) // Throws NullPointerException
Use !! only when you are certain the value is non-null and cannot prove it to the compiler. Common scenarios:
- Asserting invariants after a previous check
- Quick prototyping (but remove before production)
- Working with Java APIs that are known non-null but not annotated
Overusing !! defeats the purpose of Kotlin's null safety system.
Safe Casting (as?)
The safe cast operator returns null if the cast fails, instead of throwing a ClassCastException:
val obj: Any = "Hello"
val str: String? = obj as? String // "Hello"
val num: Int? = obj as? Int // null
This is safer than as for conditional casting:
when (val result = obj as? String) {
is String -> println("String of length ${result.length}")
null -> println("Not a string")
}
Practice Problems
Write a function safeLength that takes a nullable String? and returns its length, or 0 if null. Use the Elvis operator. Then write a version using let.
Solution
fun safeLength(name: String?): Int {
return name?.length ?: 0
}
fun safeLengthUsingLet(name: String?): Int {
return name?.let { it.length } ?: 0
}
fun main() {
println(safeLength("Kotlin")) // 6
println(safeLength(null)) // 0
} Given a nullable User object, print the user's name and email. If the user is null, print 'No user'. If the email is null, print 'No email' instead of the email. Use safe calls and the Elvis operator.
Solution
data class User(val name: String, val email: String?)
fun printProfile(user: User?) {
val name = user?.name ?: "No user"
val email = user?.email ?: "No email"
println("Name: $name, Email: $email")
}
fun main() {
printProfile(User("Alice", "alice@example.com"))
// Name: Alice, Email: alice@example.com
printProfile(User("Bob", null))
// Name: Bob, Email: No email
printProfile(null)
// Name: No user, Email: No email
} Given a nested data structure (Company has a CEO who has a name), write a function that safely retrieves the CEO name or returns 'Unknown' if any part of the chain is null.
Solution
data class Person(val name: String)
data class Company(val ceo: Person?)
fun getCeoName(company: Company?): String {
return company?.ceo?.name ?: "Unknown"
}
fun main() {
println(getCeoName(Company(Person("Andy")))) // Andy
println(getCeoName(Company(null))) // Unknown
println(getCeoName(null)) // Unknown
} Quiz
1. What does the safe-call operator (?.) do?
2. What is the result of `val x: String? = null; val y = x ?: "default"`?
3. What happens when you use the !! operator on a null value?
4. What does `name?.let { it.length }` return when name is null?
Flashcards
Question
How do you declare a nullable type in Kotlin?
Click to reveal answer
Answer
Append ? to the type. Example: val name: String? = null. Non-nullable types cannot hold null at compile time.
Question
What is the difference between ?.let and !!?
Click to reveal answer
Answer
?.let executes a block only if the value is non-null, returning null otherwise. !! converts to non-null and throws NullPointerException if null. Prefer ?.let for safe handling.
Question
When should you use the !! operator?
Click to reveal answer
Answer
Only when you are certain the value is non-null and cannot prove it to the compiler. Common in Java interop or asserting invariants. Avoid overusing it as it defeats null safety.
Question
What is a platform type in Kotlin?
Click to reveal answer
Answer
A type (marked as String!, Int!, etc.) that comes from Java code where nullability is unknown. Kotlin treats it as nullable for safety, but the compiler does not enforce null checks on it.
Revision Notes
Key Takeaways
- 1. Kotlin's type system distinguishes nullable and non-nullable types at compile time.
- 2. Use ?.let to execute code only when a value is non-null.
- 3. The Elvis operator (?:) provides defaults for nullable values.
- 4. Reserve !! for cases where nullability cannot be proven to the compiler.
- 5. Treat all Java return values as nullable unless verified with annotations.
Interview Tips
- • Explain why Kotlin's null safety is superior to Java's null handling.
- • Be ready to demonstrate safe-call chaining with ?. and ?:.
- • Discuss when !! is acceptable and when it is a code smell.
- • Know how platform types work when calling Java from Kotlin.
Cheat Sheet
Null Safety Cheat Sheet
Nullable Types:
String?can hold null;Stringcannot- Compile-time enforcement prevents NPEs
Safe Call (?.):
name?.lengthreturns Int? (null if name is null)- Chains:
user?.address?.city
Elvis (?:):
name?.length ?: 0returns 0 if name is null- Provides a default value
Non-Null Assertion (!!):
name!!.lengththrows NPE if name is null- Use sparingly; defeats null safety
Safe Let:
name?.let { block }executes only if non-nullitis the non-null value inside the block
Safe Cast (as?):
obj as? Stringreturns null if cast fails