How do I use Kotlin’s null safety features with collections and maps?

Kotlin’s null safety works especially well with collections and maps because Kotlin lets you distinguish between:

  • a nullable collection: List<String>?
  • a collection containing nullable values: List<String?>
  • both: List<String?>?

The same idea applies to maps.

1. Nullable collection vs nullable elements

val names: List<String>? = null
val nicknames: List<String?> = listOf("Ana", null, "Sam")
val maybeNicknames: List<String?>? = null

These mean different things:

List<String>?      // the list itself may be null
List<String?>      // the list exists, but elements may be null
List<String?>?     // both the list and its elements may be null

2. Safely access nullable collections

Use the safe-call operator ?. when the collection itself may be null.

val names: List<String>? = null

println(names?.size)        // null
println(names?.firstOrNull()) // null

If you want a default value, use the Elvis operator ?:.

val count = names?.size ?: 0

println(count) // 0

You can also use orEmpty() to treat a nullable collection as an empty one.

val names: List<String>? = null

for (name in names.orEmpty()) {
    println(name)
}

orEmpty() is often cleaner than repeated null checks.

3. Safely access elements

Avoid direct indexing unless you are sure the index exists.

val names = listOf("Ana", "Ben")

println(names[0]) // Ana
// println(names[5]) // IndexOutOfBoundsException

Use getOrNull() for safe access.

val names = listOf("Ana", "Ben")

val thirdName = names.getOrNull(2)

println(thirdName) // null

Combine it with Elvis for a default:

val displayName = names.getOrNull(2) ?: "Unknown"

println(displayName) // Unknown

4. Filter out null values

If a collection contains nullable elements, use filterNotNull().

val values: List<Int?> = listOf(1, null, 2, null, 3)

val nonNullValues: List<Int> = values.filterNotNull()

println(nonNullValues) // [1, 2, 3]

This is useful because Kotlin understands that the result no longer contains nullable values.

val names: List<String?> = listOf("Ana", null, "Ben")

val lengths = names
    .filterNotNull()
    .map { it.length }

println(lengths) // [3, 3]

5. Transform nullable values with mapNotNull

Use mapNotNull when your transformation may produce null and you only want valid results.

val inputs = listOf("1", "abc", "2", "", "3")

val numbers = inputs.mapNotNull { it.toIntOrNull() }

println(numbers) // [1, 2, 3]

This avoids writing:

val numbers = inputs
    .map { it.toIntOrNull() }
    .filterNotNull()

6. Use firstOrNull, singleOrNull, and find

Many Kotlin collection functions have safe nullable-returning versions.

val users = listOf("Ana", "Ben", "Chris")

val firstLongName = users.firstOrNull { it.length > 10 }
val foundUser = users.find { it.startsWith("B") }

println(firstLongName) // null
println(foundUser)     // Ben

Common safe functions include:

firstOrNull()
lastOrNull()
singleOrNull()
maxOrNull()
minOrNull()
randomOrNull()
getOrNull(index)

These return null instead of throwing when no value is available.

7. Handle nullable map values

Maps are slightly special because map[key] already returns a nullable value.

val ages: Map<String, Int> = mapOf(
    "Ana" to 28,
    "Ben" to 31
)

val anaAge = ages["Ana"]      // Int?
val missingAge = ages["Sam"]  // Int?

println(anaAge)      // 28
println(missingAge)  // null

Even if the map type is Map<String, Int>, lookup returns Int? because the key might not exist.

Use Elvis to provide a default:

val samAge = ages["Sam"] ?: 0

println(samAge) // 0

8. Distinguish missing keys from null values

If your map value type is nullable, there are two possible meanings for null:

val scores: Map<String, Int?> = mapOf(
    "Ana" to 100,
    "Ben" to null
)

println(scores["Ben"]) // null
println(scores["Sam"]) // null

Both return null, but for different reasons:

  • "Ben" exists and has a null value
  • "Sam" does not exist

Use containsKey() when you need to distinguish them.

val key = "Ben"

if (scores.containsKey(key)) {
    println("Key exists with value: ${scores[key]}")
} else {
    println("Key does not exist")
}

9. Use getValue only when the key must exist

getValue() returns a non-nullable value if the map value type is non-nullable, but throws if the key is missing.

val ages = mapOf(
    "Ana" to 28,
    "Ben" to 31
)

val age = ages.getValue("Ana")

println(age) // 28

But this throws:

val missing = ages.getValue("Sam") // NoSuchElementException

Use it when missing keys are a programming error, not normal data.

10. Safely work with nullable maps

If the map itself may be null, use ?., ?:, or orEmpty().

val ages: Map<String, Int>? = null

val anaAge = ages?.get("Ana") ?: 0

println(anaAge) // 0

Or iterate safely:

val ages: Map<String, Int>? = null

for ((name, age) in ages.orEmpty()) {
    println("$name is $age")
}

11. Combine map lookup with let

Use let to run code only when a lookup succeeds.

val ages = mapOf(
    "Ana" to 28,
    "Ben" to 31
)

ages["Ana"]?.let { age ->
    println("Ana is $age years old")
}

If the key is missing, the block is skipped.

ages["Sam"]?.let { age ->
    println("Sam is $age years old")
}

12. Use safe casts with collections

When working with mixed data, use as? and filterIsInstance.

val items: List<Any?> = listOf("Kotlin", 42, null, "Java")

val strings = items.filterIsInstance<String>()

println(strings) // [Kotlin, Java]

For a single value:

val item: Any? = "Kotlin"

val text: String? = item as? String

println(text?.uppercase()) // KOTLIN

13. Common patterns

Default empty list

fun printNames(names: List<String>?) {
    names.orEmpty().forEach { name ->
        println(name)
    }
}

Remove nulls before processing

val emails: List<String?> = listOf("[email protected]", null, "[email protected]")

val normalized = emails
    .filterNotNull()
    .map { it.lowercase() }

println(normalized)

Safe map lookup with default

val settings = mapOf(
    "theme" to "dark"
)

val theme = settings["theme"] ?: "light"

println(theme)

Safe nested lookup

val users: Map<String, Map<String, String>> = mapOf(
    "ana" to mapOf("city" to "Paris")
)

val city = users["ana"]?.get("city") ?: "Unknown"

println(city) // Paris

Quick guide

Situation Use
Collection itself may be null collection?.size, collection.orEmpty()
Element may be null filterNotNull(), ?.let { }
Index may be invalid getOrNull(index)
Need first matching item safely firstOrNull { }, find { }
Transform and skip null results mapNotNull { }
Map key may be missing map[key] ?: default
Need to know if key exists containsKey(key)
Missing key should be an error getValue(key)
Nullable map iteration map.orEmpty()

In general, prefer safe calls, Elvis defaults, orEmpty(), filterNotNull(), mapNotNull(), and safe collection accessors like getOrNull() and firstOrNull() instead of using !! or assuming values are present.