kotlinintermediate

Null Safety — Advanced Patterns

Master null safety in Kotlin: chaining, smart casts, safe builders, orElse patterns, and nullable collections.

kotlin
data class Address(val city: String?, val zip: String?)
data class Company(val name: String, val address: Address?)
data class Person(val name: String, val company: Company?, val tags: List<String>? = null)

// Extension for chaining
fun <T> T?.orElse(default: T): T = this ?: default
fun <T> T?.orThrow(message: () -> String): T = this ?: throw NoSuchElementException(message())
fun <T> T?.ifPresent(action: (T) -> Unit): T? { if (this != null) action(this); return this }

// Null-safe operations
fun String?.isNullOrShort(maxLength: Int = 3): Boolean = this == null || this.length <= maxLength

fun main() {
    // Deep null-safe chaining
    val person = Person(
        "Alice",
        Company("AcmeCorp", Address("New York", "10001"))
    )
    val nullPerson = Person("Bob", null)

    // Safe call chain
    val city = person.company?.address?.city
    println("City: $city")

    val noCity = nullPerson.company?.address?.city
    println("No city: $noCity") // null

    // Elvis operator
    val displayCity = nullPerson.company?.address?.city ?: "Unknown"
    println("Display: $displayCity")

    // Elvis with throw
    try {
        val requiredCity = nullPerson.company?.address?.city
            ?: throw IllegalStateException("City is required")
    } catch (e: Exception) {
        println("Error: ${e.message}")
    }

    // Elvis with return (in real functions)
    fun processCity(p: Person): String {
        val c = p.company ?: return "No company"
        val addr = c.address ?: return "No address"
        val city = addr.city ?: return "No city"
        return "Processing: $city"
    }
    println(processCity(person))
    println(processCity(nullPerson))

    // let for null-safe transformations
    val zip: String? = person.company?.address?.zip
    zip?.let { println("ZIP: $it") }

    // Chained let
    person.company?.address?.city?.let { city ->
        println("Uppercase city: ${city.uppercase()}")
    }

    // also for side effects
    val result = person.company?.also {
        println("Found company: ${it.name}")
    }?.address?.also {
        println("Found address in: ${it.city}")
    }

    // run for scoped computation
    val summary = person.company?.run {
        "$name at ${address?.city ?: "unknown location"}"
    } ?: "Unemployed"
    println("Summary: $summary")

    // takeIf / takeUnless
    val validZip = zip?.takeIf { it.length == 5 }
    println("Valid ZIP: $validZip")

    val name: String? = "  "
    val cleanName = name?.takeUnless { it.isBlank() }
    println("Clean name: $cleanName") // null

    // Nullable collections
    val tags: List<String>? = person.tags
    val tagCount = tags?.size ?: 0
    val firstTag = tags?.firstOrNull() ?: "none"
    println("Tags: count=$tagCount, first=$firstTag")

    // orEmpty for nullable collections
    val safeTags = tags.orEmpty()
    println("Safe tags: $safeTags")

    // filterNotNull
    val mixed: List<String?> = listOf("a", null, "b", null, "c")
    val clean: List<String> = mixed.filterNotNull()
    println("Filtered: $clean")

    // mapNotNull
    val numbers = listOf("1", "abc", "3", "xyz", "5")
    val parsed = numbers.mapNotNull { it.toIntOrNull() }
    println("Parsed: $parsed")

    // Custom extensions
    val value: String? = "Hello"
    value.ifPresent { println("Value present: $it") }
    val withDefault = (null as String?).orElse("default")
    println("With default: $withDefault")
}

Use Cases

  • Safe navigation through nested nullable objects
  • Defensive API data handling
  • Nullable collection processing

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.