Kotlin Null Safety
Preventing null pointer exceptions in Kotlin
🛡️ What is Kotlin Null Safety?
Null safety prevents null pointer exceptions by distinguishing nullable and non-nullable types. Use '?' for nullable variables and safe operators to handle potential null values securely.
// Non-nullable (safe)
val name: String = "John" // Cannot be null
// Nullable (requires special handling)
val nickname: String? = null // Can be null
val length = nickname?.length // Safe call - returns null if nickname is null
val safeName = nickname ?: "Guest" // Elvis operator - default value
Output:
name: John (never null)
nickname: null
length: null (safe call result)
safeName: Guest (default value)
Null Safety Features
Non-Nullable
Variables that cannot be null
val name: String = "John"
Nullable
Variables that can be null
val name: String? = null
Safe Calls
Safely access nullable properties
val len = name?.length
Elvis Operator
Provide default values
val result = name ?: "Default"
🔹 Nullable vs Non-Nullable Types
Understanding the difference between nullable and non-nullable variables:
// Non-nullable types (cannot be null)
val firstName: String = "Alice" // Must have a value
val age: Int = 25 // Must have a value
val isActive: Boolean = true // Must have a value
// These would cause compilation errors:
// val name: String = null // Error! String cannot be null
// val count: Int = null // Error! Int cannot be null
// Nullable types (can be null)
val middleName: String? = null // OK - can be null
val phoneNumber: String? = "555-1234" // OK - has a value
val score: Int? = null // OK - can be null
val isVerified: Boolean? = null // OK - can be null
// Nullable types require special handling
// val length = middleName.length // Error! Cannot directly access
val safeLength = middleName?.length // OK - safe call returns Int? or null
Output:
firstName: Alice (guaranteed non-null)
middleName: null
phoneNumber: 555-1234
safeLength: null
🔹 Safe Call Operator (?.)
Safely access properties and methods of nullable objects:
val user: String? = "john_doe"
val emptyUser: String? = null
// Safe calls - won't crash if null
val userLength = user?.length // 8
val emptyLength = emptyUser?.length // null
val userUpper = user?.uppercase() // "JOHN_DOE"
val emptyUpper = emptyUser?.uppercase() // null
// Chaining safe calls
val text: String? = " Hello World "
val trimmedLength = text?.trim()?.length // 11
val firstChar = text?.trim()?.firstOrNull() // 'H'
// Safe calls with method parameters
val numbers: List? = listOf(1, 2, 3, 4, 5)
val evenCount = numbers?.count { it % 2 == 0 } // 2
val nullList: List? = null
val nullCount = nullList?.count { it % 2 == 0 } // null
// Safe calls return null if any step is null
val complexChain = user?.uppercase()?.substring(0, 4)?.lowercase() // "john"
Output:
userLength: 8
emptyLength: null
userUpper: JOHN_DOE
trimmedLength: 11
evenCount: 2
complexChain: john
🔹 Elvis Operator (?:)
Provide default values when dealing with null:
val username: String? = null
val email: String? = "[email protected]"
val score: Int? = null
// Elvis operator provides default values
val displayName = username ?: "Guest" // "Guest"
val contactInfo = email ?: "No email" // "[email protected]"
val finalScore = score ?: 0 // 0
// Combining safe calls with Elvis operator
val text: String? = null
val length = text?.length ?: 0 // 0 (default for null)
val upperText = text?.uppercase() ?: "EMPTY" // "EMPTY"
// More complex examples
fun getUserInfo(user: String?): String {
return user?.let { "User: $it" } ?: "No user logged in"
}
val loggedInUser: String? = "alice"
val guestUser: String? = null
val info1 = getUserInfo(loggedInUser) // "User: alice"
val info2 = getUserInfo(guestUser) // "No user logged in"
// Elvis with early return
fun processUser(name: String?) {
val validName = name ?: return // Exit function if name is null
println("Processing user: $validName")
}
Output:
displayName: Guest
contactInfo: [email protected]
finalScore: 0
length: 0
info1: User: alice
info2: No user logged in
🔹 Not-Null Assertion (!!)
Force unwrap nullable values (use with caution):
val nullableText: String? = "Hello Kotlin"
val anotherText: String? = null
// Not-null assertion - converts nullable to non-nullable
val definitelyText: String = nullableText!! // "Hello Kotlin"
val length = definitelyText.length // 12
// WARNING: This will crash if the value is null!
// val crashExample = anotherText!! // KotlinNullPointerException!
// Safe usage - only when you're absolutely sure it's not null
fun processNonNullString(text: String?) {
// Check first, then assert
if (text != null) {
val safeText = text!! // Safe because we checked
println("Processing: $safeText")
}
}
// Better alternatives to !! operator
val betterApproach1 = nullableText ?: "default" // Elvis operator
val betterApproach2 = nullableText?.let { it.length } // Safe call with let
// When !! might be appropriate (rare cases)
val systemProperty = System.getProperty("user.name")!! // System property should exist
val resourceFile = javaClass.getResource("/config.txt")!! // Resource must exist
Output:
definitelyText: Hello Kotlin
length: 12
⚠️ Warning: !! can cause crashes if value is null
betterApproach1: Hello Kotlin
betterApproach2: 12
🔹 Smart Casts and Null Checks
Kotlin automatically casts variables after null checks:
fun processUser(name: String?) {
// Before null check - name is String?
// val length = name.length // Error! Cannot access directly
if (name != null) {
// After null check - name is automatically cast to String
val length = name.length // OK! Smart cast to non-null
val upper = name.uppercase() // OK! Can call String methods
println("User: $name, Length: $length")
}
}
fun checkAndProcess(text: String?) {
// Multiple ways to check for null
// Method 1: Traditional null check
if (text != null && text.isNotEmpty()) {
println("Text: ${text.uppercase()}") // Smart cast to String
}
// Method 2: Using when expression
when {
text == null -> println("Text is null")
text.isEmpty() -> println("Text is empty")
else -> println("Text: ${text.lowercase()}") // Smart cast
}
}
// Smart casts work with various conditions
fun smartCastExamples(value: Any?) {
if (value is String) {
// Smart cast to String
println("String length: ${value.length}")
}
if (value != null && value is Int) {
// Smart cast to Int
println("Integer value: ${value + 10}")
}
}
Output:
processUser("Alice") → User: Alice, Length: 5
processUser(null) → (no output)
Smart casts eliminate need for explicit casting
🔹 Null Safety Best Practices
Guidelines for effective null safety in Kotlin:
Best Practices:
- Prefer non-nullable types: Use nullable only when necessary
- Use safe calls (?.): Instead of null checks when possible
- Provide defaults with Elvis (?:): Better than returning null
- Avoid !! operator: Use only when absolutely certain
- Initialize variables: Provide initial values when possible
// Good practices
class User(
val id: Int, // Non-nullable - always has value
val name: String, // Non-nullable - required field
val email: String?, // Nullable - optional field
val phone: String? = null // Nullable with default
) {
// Safe property access
val displayName: String
get() = name.ifEmpty { "Unknown User" }
// Safe method with default
fun getContactInfo(): String {
return email ?: phone ?: "No contact information"
}
// Safe processing
fun sendNotification(message: String) {
email?.let { emailAddress ->
println("Sending email to $emailAddress: $message")
} ?: run {
println("Cannot send notification - no email address")
}
}
}
// Usage
val user = User(1, "Alice", "[email protected]")
val userWithoutEmail = User(2, "Bob", null, "555-1234")
println(user.getContactInfo()) // [email protected]
println(userWithoutEmail.getContactInfo()) // 555-1234