Kotlin Null Safety — Complete Guide with Examples
In this tutorial, you will learn about Kotlin Null Safety. We cover key concepts, practical examples, and best practices to help you master this topic.
Kotlin null safety distinguishes nullable and non-nullable types at the compiler level, using safe calls (?.), the Elvis operator (?:), the not-null assertion (!!), and smart casts to eliminate NullPointerException from production code.
What You'll Learn
- Declare nullable and non-nullable types
- Use safe calls (?.) for chained access
- Apply the Elvis operator (?:) for default values
- Understand the not-null assertion (!!) and its risks
- Use smart casts after null checks
- Work with the let function for null-safe execution
- Handle platform types when calling Java code
Why It Matters
NullPointerException is the most common crash in Java applications. Studies show it accounts for 20-30% of all production crashes. Kotlin solves this at the language level by making null a type-system concept. Non-nullable types guarantee that a variable will never be null. The compiler enforces this, eliminating entire categories of bugs. For Android developers, this means fewer ANRs and crashes. For server-side developers, it means more reliable services.
Real-World Use
DodaTech's Android utility app reduced crash rate by 40% after migrating from Java to Kotlin, primarily due to null safety. Backend API responses often contain optional fields. Kotlin's nullable types model these naturally, and safe calls prevent crashes when optional data is missing.
Learning Path
flowchart LR A[Functions] --> B[Null Safety\nYou are here] B --> C[Collections] style B fill:#f90,color:#fff
Nullable versus Non-Nullable Types
By default, Kotlin types cannot hold null. Add ? to make a type nullable.
fun main() {
// Non-nullable: cannot be null
var name: String = "Alice"
// name = null // Compilation error
// Nullable: can be null
var nullableName: String? = "Bob"
nullableName = null // OK
// Non-nullable function parameter
fun printLength(text: String) {
println(text.length)
}
// printLength(null) // Compilation error
// Nullable function parameter
fun printNullableLength(text: String?) {
println(text?.length ?: 0)
}
printNullableLength(null) // Output: 0
printNullableLength("Hello") // Output: 5
}
Output: The non-nullable parameter rejects null at compile time. The nullable parameter handles both null and non-null values safely.
Safe Calls (?.)
The safe call operator accesses a property or method only if the receiver is non-null. Otherwise, it returns null.
data class Address(val street: String, val city: String?)
data class Person(val name: String, val address: Address?)
fun main() {
val person1: Person? = Person("Alice", Address("123 Main St", null))
val person2: Person? = null
// Chained safe calls
val city1 = person1?.address?.city
val city2 = person2?.address?.city
println(city1) // Output: null (city is null)
println(city2) // Output: null (person is null)
// Safe call with function invocation
val length = person1?.name?.length
println(length) // Output: 5
// Safe call on nullable list
val numbers: List<Int>? = listOf(1, 2, 3)
val size = numbers?.size
println(size) // Output: 3
}
Output: The safe call chain returns null if any intermediate value is null, without throwing an exception.
Elvis Operator (?:)
The Elvis operator provides a default value when the expression on the left is null.
fun main() {
val name: String? = null
val displayName = name ?: "Guest"
println(displayName) // Output: Guest
val actualName: String? = "Alice"
val displayActual = actualName ?: "Guest"
println(displayActual) // Output: Alice
// Elvis with early return
fun getUserEmail(userId: Int): String? {
// Simulated lookup
return if (userId == 1) "alice@example.com" else null
}
val email = getUserEmail(2) ?: return
println(email) // This line never executes because return runs first
}
Output: The Elvis operator substitutes "Guest" when the name is null. It returns the original value otherwise.
The Elvis operator can also be used with return or throw:
fun processUser(user: User?) {
val name = user?.name ?: throw IllegalArgumentException("User must have a name")
println("Processing: $name")
}
Not-Null Assertion (!!)
The double-bang operator converts any nullable type to a non-nullable type, throwing NullPointerException if the value is null.
fun main() {
var text: String? = "Hello"
val length = text!!.length
println(length) // Output: 5
text = null
// val crash = text!!.length // Throws NullPointerException
// Practical use: when you know a value is non-null
val numbers: List<Int>? = listOf(1, 2, 3)
val first = numbers!!.first()
println(first) // Output: 1
}
Output: The not-null assertion works when the value is non-null. Using it on a null value crashes the program.
The !! operator is a code smell. Prefer safe calls, Elvis, or let instead. Use !! only when you are certain the value is non-null and the compiler cannot infer it.
Smart Casts
Kotlin automatically casts nullable types to non-nullable after a null check.
fun describeString(text: String?) {
if (text != null) {
// Smart cast: text is now String (non-nullable)
println("Length: ${text.length}, Uppercase: ${text.uppercase()}")
} else {
println("Text is null")
}
}
fun main() {
describeString("Kotlin") // Output: Length: 6, Uppercase: KOTLIN
describeString(null) // Output: Text is null
// Smart cast with when
val value: Any? = "Hello"
when (value) {
is String -> println("String of length ${value.length}")
is Int -> println("Number: $value")
null -> println("Is null")
else -> println("Unknown type")
}
// Output: String of length 5
}
Output: After the null check, the compiler treats text as non-nullable String, allowing direct access to length and uppercase.
The let Function for Null Safety
Combine let with the safe call operator to execute code only on non-null values.
data class User(val id: Int, val name: String)
fun findUserById(id: Int): User? {
return if (id == 1) User(1, "Alice") else null
}
fun main() {
val user = findUserById(1)
// Execute block only if user is not null
user?.let {
println("Found user: ${it.name} (ID: ${it.id})")
// Multiple operations on non-null user
val uppercaseName = it.name.uppercase()
println("Uppercase: $uppercaseName")
}
// Output: Found user: Alice (ID: 1)
// Uppercase: ALICE
val missingUser = findUserById(2)
missingUser?.let {
println("This will not print")
}
// No output: let block is skipped
}
Output: The let block executes only when the user is non-null. This is the idiomatic way to handle nullable values in Kotlin.
Platform Types
When calling Java code from Kotlin, Java types become platform types. The compiler does not enforce null safety on them.
// Java class
// public class JavaUser {
// private String name;
// public String getName() { return name; }
// }
// Kotlin call site
fun processJavaUser(javaUser: JavaUser?) {
// Platform type: String! (nullable or non-nullable unknown)
val name = javaUser?.name
// You decide the nullability
val safeName: String? = name
val unsafeName: String = name ?: "Unknown"
}
Output: Platform types have a ! suffix in error messages. They require explicit null handling at the Kotlin boundary.
Use nullable types for Java values that might be null. Add @Nullable and @NotNull annotations to Java code for better interop.
Common Mistakes
Overusing !! everywhere: Using !! defeats null safety and reintroduces NPE risk. Reserve it for rare cases where you are absolutely certain the value is non-null.
Ignoring the Elvis operator: Instead of writing
if (x != null) x else defaultValue, usex ?: defaultValue. It is shorter and more idiomatic.Forgetting to handle platform types: When calling Java APIs, assume every return value might be null unless annotated otherwise.
Using safe calls when null is not valid: If a null value indicates a bug, fail early with requireNotNull or checkNotNull rather than silently propagating null.
Nesting let calls: Multiple nested let calls create deeply indented code. Use chained safe calls or extract logic into separate functions.
Smart cast limitations with mutable properties: Smart casts work only on val properties that are not subject to change between check and use. For var properties, use safe calls or let.
Practice Questions
- What is the difference between String and String? in Kotlin?
Answer: String is a non-nullable type that cannot hold null. String? is a nullable type that can hold either a String value or null.
- When should you use the !! operator?
Answer: Only when you are certain a nullable value is non-null at the point of access and the compiler cannot verify it. Typical cases include values checked earlier in the function or values from well-known APIs.
- What does the Elvis operator (?:) do?
Answer: It returns the left-hand value if it is non-null, otherwise it returns the right-hand value. It provides a default for nullable expressions.
- How does a smart cast work?
Answer: After a null check or type check, the compiler automatically casts the variable to the checked type, eliminating the need for explicit casts.
- Challenge: Write a function that takes a list of nullable strings, filters out null values, converts the remaining strings to uppercase, and returns the result as a single comma-separated string. Handle the case where the list itself is null.
Answer:
fun processStrings(strings: List<String?>?): String {
return strings
?.filterNotNull()
?.map { it.uppercase() }
?.joinToString(", ")
?: "List was null"
}
fun main() {
println(processStrings(listOf("apple", null, "banana", null, "cherry")))
// Output: APPLE, BANANA, CHERRY
println(processStrings(null))
// Output: List was null
}
Mini Project
Create a safe configuration loader that reads environment variables and provides typed access with null safety. Requirements:
- Define a Config class that wraps a Map<String, String?>
- Implement getString(key, default) with Elvis operator
- Implement getInt(key, default) with safe conversion
- Implement getRequiredString(key) that throws on null
- Implement getStringOrNull(key) using safe access
- Use let for executing actions only when a config value exists
- Write tests that cover null, missing, and present keys
This project applies all null safety mechanisms in a practical, real-world pattern.
FAQ
What's Next
Now that you understand null safety, learn collections for working with lists, sets, and maps. You can also explore lambdas to write functional-style collection operations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro