Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 10 minutes
  • Examples run with Kotlin 2.4.20
  • By MySmartCoPilot
Free core, Pro depth: 4 of 10 minutes free
Free: A question mark on the items and one on the list mean different things · filterNotNull, mapNotNull and firstNotNullOfOrNull clean nullable data · A type parameter can stand for a nullable type. With a pass: 2 in-depth parts and 3 interview answers, about 6 minutes.

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:

Shapes.kt: three places for a question mark Kotlin · shapes/Shapes.kt
// 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

  1. Question 1 of 2 Which type allows the list itself to be null, but never one of its items?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: List<String>?

    A question mark belongs to the type it follows: after String it lets an item be null, after the list type it lets the whole list be null.

  2. Question 2 of 2 coPassengers is a List<String>? that holds null. What do coPassengers?.size and coPassengers.orEmpty().size give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: null, then 0

    ?. keeps the missing list as null for the caller to handle, and orEmpty() decides that a missing list means an empty one, as the last line of Shapes.kt printed [].

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:

Readings.kt: seven readings, cleaned three ways Kotlin · readings/Readings.kt
// 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:

Interactive · step by step mapNotNull over seven readings typed in by hand Step through raw.mapNotNull { it?.toIntOrNull() } one reading at a time. A reading is kept only when the block gives a number.
mapNotNull over seven readings typed in by hand, step 8 of 8. Showing "1042" (parsed: 1042, kept), null (null: dropped), "1057" (parsed: 1057, kept), "" (blank) (not a number: dropped), "10x5" (not a number: dropped), "1063" (parsed: 1063, kept), "1071" (parsed: 1071, kept), 1042, 1057, 1063, 1071. In focus: 1042, 1057, 1063, 1071."1042"parsed: 1042, keptnullnull: dropped"1057"parsed: 1057, kept"" (blank)not a number: dropped"10x5"not a number: dropped"1063"parsed: 1063, kept"1071"parsed: 1071, kept1042105710631071

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. 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.
  2. A number. "1042" is not null, so it?.toIntOrNull() parses it and gives 1042. The result is not null, so mapNotNull keeps it.
  3. A missing reading. The second reading is null, so the safe call skips toIntOrNull and the block gives null. mapNotNull drops it.
  4. Another number. "1057" parses to 1057, which is kept as the second item of the result.
  5. A blank. The blank is a String, not null, so toIntOrNull runs, but an empty text is no number and it gives null. Dropped.
  6. A typing mistake. "10x5" has a letter in it, so toIntOrNull gives null here too. Dropped, without an exception.
  7. The last two. "1063" and "1071" both parse, and both are kept, in the order they came.
  8. 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

  1. Question 1 of 2 raw is ["1042", null, "1057", "", "10x5", "1063", "1071"]. How many items does raw.filterNotNull() keep?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Six, the blank and "10x5" included

    filterNotNull removes nulls and nothing else. To drop text that is not a number as well, parse inside mapNotNull.

  2. Question 2 of 2 What does listOf("x", null, "7", "8").firstNotNullOfOrNull { it?.toIntOrNull() } give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 7

    firstNotNullOfOrNull runs its block item by item and returns the first result that is not null, or null when there is none.

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?>:

Generic.kt: T can be String?, and T & Any never is Kotlin · generic/Generic.kt
// 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

  1. One question fun <T> firstItem(items: List<T>): T = items.first() is called with a List<String?> whose first item is null. What happens?

    Choose one answer.

    Show the answer: One question

    Answer: It returns null; T is String?, so the result has the type String?

    A type parameter takes the caller's type, a nullable one included, unless a bound such as T : Any rules that out.

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.

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.

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:

ParseScores.kt: five checks, two of them fail Kotlin · practice/ParseScores.kt
// 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.

  1. Question 1 of 6 orders is a List<String>? from an old API. Which of these lines compiles and prints 0 when orders is null?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: println(orders?.size ?: 0)

    A ? on the list asks for ?. on the list. orders?.size ?: 0, or orders.orEmpty().size, reads a missing list as an empty one.

  2. Question 2 of 6 nominees is a List<String?>? holding [null, "Ravi"]. What does nominees?.get(1)?.length give?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: 4

    In a List<String?>?, both the list and each item may be missing, so reading an item's length needs ?. twice.

  3. Question 3 of 6 raw is a List<String?>. What is the type of raw.mapNotNull { it?.toIntOrNull() }?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: List<Int>

    mapNotNull { … } is map followed by filterNotNull in one pass, and its result type is the block's type without null.

  4. Question 4 of 6 A report needs the readings as numbers and the count of readings it could not use. Which code gives both?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: val readings = raw.mapNotNull { it?.toIntOrNull() }, then raw.size - readings.size

    Parse with toIntOrNull inside mapNotNull, and compare the sizes to know how much was dropped.

  5. Question 5 of 6 What is the upper bound of T in fun <T> firstItem(items: List<T>): T?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: Any?, so T can be a nullable type

    The default upper bound of a type parameter is Any?. Write <T : Any> to keep nullable types out.

  6. Question 6 of 6 Which of these functions refuses a List<String?> as its argument?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: fun <T : Any> f(items: List<T>): T

    A bound acts at the call: T : Any turns away every nullable type argument, while T & Any changes only the type it appears in, and a bound of Any? allows everything, as no bound does.

Interview questions

  1. 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 be null, so list.size works but an item needs a safe call, as in list[0]?.length. List<String>? is a list that may be missing as a whole, whose items are never null, so the list needs ?. before any call, as in list?.size. List<String?>? allows both. Choose by what can really be missing: optional middle names are a List<String?>, while a list that may not exist is a List<String>?. filterNotNull() turns a List<String?> into a List<String>, and orEmpty() turns a missing list into an empty one.

  2. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • List<String?> may hold null items, List<String>? may be null as a whole, and List<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, and firstNotNullOfOrNull { … } stops at the first value its block gives.
  • A type parameter T can be a nullable type unless it is bounded with T : Any, and T & Any is T without 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.