Kotlin Module 3 – Null safety
Nulls inside collections and generics
Tell List<String?> from List<String>?, clean nullable data with filterNotNull and mapNotNull, and write T & Any for the non-null version of a type parameter.
What you will learn
- Distinguish List<String?>, List<String>? and List<String?>?, and read each one safely
- Clean nullable data with filterNotNull, mapNotNull and firstNotNullOfOrNull
- Express the non-null version of a type parameter with T & Any, and keep nullable types out with T : Any
- Choose between null, an empty list and an exception for a collection that an API returns
Before you start
What you’ll learn · Before you start
What you will learn
- Distinguish List<String?>, List<String>? and List<String?>?, and read each one safely
- Clean nullable data with filterNotNull, mapNotNull and firstNotNullOfOrNull
- Express the non-null version of a type parameter with T & Any, and keep nullable types out with T : Any
- Choose between null, an empty list and an exception for a collection that an API returns
Before you start
On this page
An electricity meter app uploads seven readings that a field worker typed in: one never arrived, one was left blank and
one was mistyped as 10x5. Stored as a List<String?>, the type says that any reading may be missing; stored as a
List<String>?, it says something else, that the whole list may be missing.
You will tell those types apart and read each one safely, clean nullable data in one pass with filterNotNull,
mapNotNull and firstNotNullOfOrNull, and see how generics carry nullability: a type parameter T can stand for a
nullable type, and T & Any is its version without null.
A question mark on the items and one on the list mean different things
Nulls inside collections come in two places, and the type says which. A List<String?> is a list that is always
there, whose items may be null; a List<String>? is a list that may be missing as a whole, with items that never
are; and a List<String?>? allows both. Three lists from a booking form:
// Three lists from a booking form: where the ? goes decides what can be missing.
fun main() {
// Items may be missing: a middle name is optional for each passenger.
val middleNames: List<String?> = listOf("Kumar", null, "Devi")
// The whole list may be missing: nobody has filled in the co-passengers yet.
val coPassengers: List<String>? = listOf<List<String>?>(null).first()
// Both: the nominee list may be missing, and so may each nominee.
val nominees: List<String?>? = listOf(null, "Ravi")
println("middle names: ${middleNames.size}, length of the second: ${middleNames[1]?.length}")
println("co-passengers: ${coPassengers?.size ?: 0}")
println("first nominee: ${nominees?.get(0)?.length}, second: ${nominees?.get(1)?.length}")
println("co-passengers, or an empty list: ${coPassengers.orEmpty()}")
} Output
middle names: 3, length of the second: null co-passengers: 0 first nominee: null, second: 4 co-passengers, or an empty list: []
Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Shapes.kt -include-runtime -d main.jar && java -jar main.jar
Each ? in a type asks for a ?. in the code where it stands: middleNames.size needs none, an item of that list
does, and the co-passenger list needs one before size. A nominee’s length needs two, because everything after a
safe call is nullable.
| Type | The list can be null |
An item can be null |
The first item’s length |
|---|---|---|---|
List<String?> |
no | yes | list[0]?.length |
List<String>? |
yes | no | list?.get(0)?.length |
List<String?>? |
yes | yes | list?.get(0)?.length |
orEmpty() turns a missing list into an empty one, which the code then reads like any other list: the last line
printed [].
Checkpoint
Check: Where the question mark goes
filterNotNull, mapNotNull and firstNotNullOfOrNull clean nullable data
Nulls inside collections usually have to go before the values can be used. The function filterNotNull() drops the
items that are null. mapNotNull { … } runs its block on each item and keeps the results that are not null, so it can parse and
clean in a single pass. firstNotNullOfOrNull { … } stops at the first item whose block gives a value:
// Meter readings typed in by hand: one never arrived, one is blank and one is mistyped.
fun main() {
val raw: List<String?> = listOf("1042", null, "1057", "", "10x5", "1063", "1071")
// Parse each reading and drop every one that is missing or not a number, in one pass.
val readings: List<Int> = raw.mapNotNull { it?.toIntOrNull() }
println("kept ${readings.size} of ${raw.size}: $readings")
println("dropped ${raw.size - readings.size}")
// filterNotNull drops only the nulls: the blank and the mistyped text stay.
println("not null: ${raw.filterNotNull()}")
// The first reading that parses, without parsing the rest.
println("first number: ${raw.firstNotNullOfOrNull { it?.toIntOrNull() }}")
println("units used: ${readings.last() - readings.first()}")
} Output
kept 4 of 7: [1042, 1057, 1063, 1071] dropped 3 not null: [1042, 1057, , 10x5, 1063, 1071] first number: 1042 units used: 29
Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Readings.kt -include-runtime -d main.jar && java -jar main.jar
Step through the mapNotNull call one reading at a time:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Seven readings. raw is a List<String?>: seven readings as they were typed in, one of them null because it never arrived. mapNotNull runs its block on each one, in order.
- A number. "1042" is not null, so it?.toIntOrNull() parses it and gives 1042. The result is not null, so mapNotNull keeps it.
- A missing reading. The second reading is null, so the safe call skips toIntOrNull and the block gives null. mapNotNull drops it.
- Another number. "1057" parses to 1057, which is kept as the second item of the result.
- A blank. The blank is a String, not null, so toIntOrNull runs, but an empty text is no number and it gives null. Dropped.
- A typing mistake. "10x5" has a letter in it, so toIntOrNull gives null here too. Dropped, without an exception.
- The last two. "1063" and "1071" both parse, and both are kept, in the order they came.
- One pass, a List<Int>. Four of the seven readings are kept and three dropped, in one pass. The result is a List<Int>: nothing in it can be null, so the rest of the code needs no ?. to read it.
filterNotNull() kept the blank and “10x5”: they are strings, not nulls. mapNotNull { it?.toIntOrNull() } dropped
all three gaps, and its result is a List<Int>, so the rest of the code reads it without a single ?.: the 29 units
used came straight from it.
Key idea
Clean nullable data in one pass where it arrives, and count what you dropped: mapNotNull gives a list with no
nulls in it, and the count tells the reader how much was missing.
Checkpoint
Check: filterNotNull, mapNotNull and firstNotNullOfOrNull
A type parameter can stand for a nullable type
Generics carry nullability through to their callers. A
The placeholder T in a declaration such as fun <T> firstItem(items: List<T>): T. Each call fills it with a real type, here the type of the list's items.
T with no bound can be any type, nullable types included, because its default upper bound is Any?. So
firstItem(nominees) returns a String? when the list is a List<String?>:
// A type parameter takes whatever type its caller has, a nullable one included.
fun <T> firstItem(items: List<T>): T = items.first()
// T & Any is "T without null": the result is never null, whatever T is.
fun <T> firstPresent(items: List<T>): T & Any =
items.firstNotNullOfOrNull { it } ?: throw NoSuchElementException("no item is present")
fun main() {
val nominees: List<String?> = listOf(null, "Ravi")
// Here T is String?, so the result is a String? and needs ?. to read.
val first = firstItem(nominees)
println("firstItem gave $first, and first?.length gives ${first?.length}")
// Here the result is a String, so .length needs no ?.
val present = firstPresent(nominees)
println("firstPresent gave $present, and present.length gives ${present.length}")
} Output
firstItem gave null, and first?.length gives null firstPresent gave Ravi, and present.length gives 4
Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Generic.kt -include-runtime -d main.jar && java -jar main.jar
T & Any is the type T without null, a definitely non-nullable type: A type written T & Any for a type parameter T: the type T without null, whatever type a caller gives T. A function can return T & Any even when T itself may be nullable.,
stable since Kotlin 1.7.0. firstPresent returns one whatever T is, so present.length needs no ?.. A bound,
<T : Any>, goes further and refuses nullable types for T altogether. With generics, choose by what the function
accepts: T : Any when null has no place in it, and plain T with T & Any in the result when it accepts nulls and
removes them.
Checkpoint
Check: A type parameter that can be nullable
Missing, null or empty: collections and maps in an API, measured Pro: opens with a Pro, Premium or Ultimate pass
A collection in an API can be missing, empty, or full of nulls, and its caller has to tell the three apart. This part measures where the standard library mixes them up, and shows the types and functions that keep them apart.
In this part:
- firstOrNull cannot tell an empty list from a null first item
- A map with null values holds two kinds of null
- A failure drill: a missing reading counted as zero
3 runnable examples (Kotlin) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Generic functions that keep null where it belongs, with T & Any Pro: opens with a Pro, Premium or Ultimate pass
Generic code decides, once and for every caller, what may be null. This part compares four signatures that say it in
different ways, writes two helpers with T & Any, and shows what erased item types hide from an is check.
In this part:
- Four generic signatures, compared
- Two helpers written with T & Any
- Erased item types: what an is check can see
3 runnable examples (Kotlin) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: scores from typed-in marks
This program checks a function that turns typed-in marks into scores from 0 to 100 and counts what it rejected. As given, two of its five checks fail:
// Practice: fix parseScores so that all five checks pass. It returns the valid scores, 0 to 100, in their order,
// and the number of entries it rejected: missing, blank, not a number or out of range.
fun parseScores(raw: List<String?>): Pair<List<Int>, Int> {
val scores = raw.mapNotNull { it?.toIntOrNull() }
return scores to raw.size - scores.size
}
fun main() {
val checks = listOf(
listOf("90", null, "100") to (listOf(90, 100) to 1),
listOf("", "abc") to (emptyList<Int>() to 2),
listOf("-5", "7") to (listOf(7) to 1),
listOf("101", "0") to (listOf(0) to 1),
emptyList<String?>() to (emptyList<Int>() to 0),
)
var passed = 0
for ((raw, want) in checks) {
val got = parseScores(raw)
val ok = got == want
if (ok) passed++
println("${if (ok) "ok " else "FAIL"} parseScores($raw) = $got, want $want")
}
println("$passed of ${checks.size} checks pass")
if (passed < checks.size) kotlin.system.exitProcess(1)
} Output (exit status 1)
ok parseScores([90, null, 100]) = ([90, 100], 1), want ([90, 100], 1) ok parseScores([, abc]) = ([], 2), want ([], 2) FAIL parseScores([-5, 7]) = ([-5, 7], 0), want ([7], 1) FAIL parseScores([101, 0]) = ([101, 0], 0), want ([0], 1) ok parseScores([]) = ([], 0), want ([], 0) 3 of 5 checks pass
Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc ParseScores.kt -include-runtime -d main.jar && java -jar main.jar
Copy it, fix parseScores, and run it with
kotlinc ParseScores.kt -include-runtime -d scores.jar && java -jar scores.jar until all five checks pass. First hint:
a number outside 0 to 100 should give null inside the mapNotNull block, so that it is dropped and counted like a
blank.
The questions below cover the three ideas of the first three sections, the one you know least first:
Practice that adapts to you
Practise this lesson
6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher · Warm-up
What is the difference between List<String?> and List<String>? in Kotlin?
They put the question mark in different places.
List<String?>is a list that is always there, whose items may benull, solist.sizeworks but an item needs a safe call, as inlist[0]?.length.List<String>?is a list that may be missing as a whole, whose items are nevernull, so the list needs?.before any call, as inlist?.size.List<String?>?allows both. Choose by what can really be missing: optional middle names are aList<String?>, while a list that may not exist is aList<String>?.filterNotNull()turns aList<String?>into aList<String>, andorEmpty()turns a missing list into an empty one. -
Mid to senior
An app's API returns List<Order>?, where null was meant to mean "not loaded yet" and an empty list "no orders". A bug report says customers saw "You have no orders" while their orders were still loading. How would you redesign the type, and how would you move the callers over?
-
Fresher to mid
A billing job computes daily electricity use as the difference between consecutive meter readings, after replacing each missing reading with 0. One customer's statement shows a day with a negative use, followed by a day as large as the meter's whole count. What happened, and how would you compute and show daily use instead?
-
Mid to senior
Write the signature of a generic helper that takes a list whose items may be null and returns the first item that is present, or throws when there is none. Why T & Any rather than a bound T : Any, and when would you choose the bound?
Key takeaways
List<String?>may holdnullitems,List<String>?may benullas a whole, andList<String?>?may be both; each?asks for a?.where it stands.filterNotNull()removes only the nulls,mapNotNull { … }parses and cleans in one pass and gives a list without nulls, andfirstNotNullOfOrNull { … }stops at the first value its block gives.- A type parameter
Tcan be a nullable type unless it is bounded withT : Any, andT & AnyisTwithout null.
Terms in this lesson
- definitely non-nullable type
- A type written T & Any for a type parameter T: the type T without null, whatever type a caller gives T. A function can return T & Any even when T itself may be nullable.
References
- Null safety: collections of a nullable type (JetBrains (kotlinlang.org))
- Collection transformation operations: mapNotNull (JetBrains (kotlinlang.org))
- firstNotNullOfOrNull (Kotlin standard library) (JetBrains (kotlinlang.org))
- Generics: upper bounds, definitely non-nullable types and type erasure (JetBrains (kotlinlang.org))
- What's new in Kotlin 1.7.0: stable definitely non-nullable types (JetBrains (kotlinlang.org))
- Map-specific operations: retrieve keys and values (JetBrains (kotlinlang.org))
- What's new in Kotlin 2.4.0: map fallback functions for null values and missing keys (JetBrains (kotlinlang.org))
Report a problem with this lesson
Kept only in this browser. Your Learn progress