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.
