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

🧠 Test Your Knowledge

Which operator safely accesses properties of nullable objects?