---
title: 'Null Safety'
source: 'https://academia.sh/en/courses/kotlin/null-safety'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Shows the distinction between String and String? through the compiler's rejection"
    - 'Measures that a smart cast narrows the type after a null check'
    - 'Shows that two null values count as the same object and the criterion comes out singleton'
    - 'Verifies with the bounding measurement that the criterion stops at the first question and the later questions are never consulted'
---

# Null Safety

A measurement of how nullable and non-nullable types are separate types, how a type narrows after a null check, and which criterion null equal to null produces.

The previous lesson measured which value a scope function returns. Sometimes what comes back is nothing at all: a search finds no result, a field has not been filled in, a conversion cannot finish. That situation can be expressed with a value such as `null`; the value then has to be checked before every use, and when the check is forgotten the error surfaces at run time.

Kotlin writes absence into the type itself. A `String` is always a string; a `String?` is either a string or `null`. These two types are not the same type, and the compiler does not let you use one in place of the other. This is called **null safety**: a nullable type and a non-nullable type are separate types, so every place where `null` is possible is visible in the source code. This lesson measures that distinction through the rejection the compiler issues, then looks at what the same distinction says about the equality question.

The course's measurement axis is the relationship between a value and another value that counts as equal to it. A **criterion** takes one of four forms: **singleton**, **identity**, **content**, and **partial content**. The rule works in order. If the first value and its twin are the same object, the criterion is singleton. If not, and the twin is not equal, it is identity: only the same object counts as equal. If the twin is equal but a third value that differs from the first in one respect is not equal, it is content: the objects are separate and the equality comes from the content. If the third value is also equal, it is partial content: the respect in which it differs does not affect equality. In the binding measurement of the opening lesson the distribution over eight values was 2/2/3/1; this lesson measures only its own slice.

The `Criterion` class that does the classification is given below as it is. Code blocks are compiled and run only inside the sandbox; every line in the output blocks is what the sandbox printed.

```kotlin
// Criterion.kt — classifies the criterion that makes two values equal
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
    val sameObject = first === twin
    val equalTwin = first == twin
    val equalOther = first == other

    val name: String = when {
        sameObject -> "singleton"
        !equalTwin -> "identity"
        !equalOther -> "content"
        else -> "partial-content"
    }

    fun report() {
        println("$label: same-object=$sameObject, twin=$equalTwin, other=$equalOther -> $name")
    }
}

fun summarize(checks: List<Criterion>) {
    for (name in listOf("singleton", "identity", "content", "partial-content")) {
        println("$name: ${checks.count { it.name == name }}")
    }
}
```

The parameters have type `Any?`, which means `null` can be an argument too. This lesson's first question is exactly that: when `null` is treated as a value, which criterion does it produce?

## Two Separate Types

In the block below, a `String` and a `String?` with the same content stand side by side. The comparison compiles, because `==` does not require the two sides to have matching types; it looks only at the content. But an operation that depends on the type, such as taking the length, is trouble-free only on the non-nullable type.

```kotlin
// Main.kt — two types
fun main() {
    val a: String = "cube"
    val b: String? = "cube"
    println(a == b)
    println(a.length)
    val c: String? = null
    println(c == null)
    println(c)
}
```

```
true
4
true
null
```

`a == b` is true: both strings carry the same content, and the filled-in state of the `String?` type is compared as a string. `c` carries `null`, and `null` is written to the screen; printing a nullable variable is not an error. The point to notice is that in the `a.length` line `a` is a non-nullable type. Writing the same line with `b` would lead to a compiler rejection.

There are two rejections, and both were measured in the sandbox. The first is assigning `null` to a variable of type `String`. In Kotlin 2.4.21 the compiler rejects this: `null` cannot be a value of a non-nullable type. The error appears at compile time, and the program never runs.

```kotlin
fun main() {
    val text: String = null
    println(text)
}
```

The second is asking for `length` directly on a variable of type `String?`. The compiler rejects this as well: when the receiver is a nullable type, only a safe call or a not-null assertion is allowed. The safe call is the subject of the next lesson; the not-null assertion is also measured there, together with its failure.

```kotlin
fun main() {
    val text: String? = "cube"
    println(text.length)
}
```

**TN1.** These two rejections were measured only for a local variable; they were not tried separately for a class property, a function parameter, or a return type. The error text depends on the compiler version and is not reproduced here; what was measured is that the program never runs.

## The Type Narrows After a Null Check

To get the length of a `String?` value, you first have to make sure it is not `null`. Kotlin does not make you write a separate conversion for this. When a `null` check is made inside an `if`, the compiler narrows the variable's type to `String` within that branch. This behavior is called a **smart cast**: the programmer writes no conversion, and the check itself changes the type.

In the block below, the `lengthOf` function takes a `String?`; inside the `if (text != null)` branch, `text.length` is called directly. The block also filters a list containing two `null` values with `filterNotNull`; the result is a `List<String>` rather than a `List<String?>`, and its elements give `length` without any conversion.

```kotlin
// Main.kt — smart cast
fun lengthOf(text: String?): Int {
    if (text != null) {
        return text.length
    }
    return -1
}

fun main() {
    println(lengthOf("cube"))
    println(lengthOf(null))
    val values = listOf("a", null, "bb", null)
    println(values.size)
    val present = values.filterNotNull()
    println(present.size)
    println(present.sumOf { it.length })
}
```

```
4
-1
4
2
3
```

`lengthOf("cube")` gives four, and `lengthOf(null)` gives `-1`. The list has four elements, but `filterNotNull` discards the two `null` values and leaves two strings; the sum of their lengths is three. What is measured here is that a `null` check is not merely a condition: after the check, the same variable is a different type.

**TN2.** The smart cast was measured only through `if` and `filterNotNull`; it was not tried for a check inside `when`, for checks chained with `&&`, or for when the narrowing breaks with mutable variables. The narrowing holds as long as the variable is known not to change between the check and the use.

## Where the Question Mark Goes

The `?` mark can be written in only one place of a type, and where it goes changes the meaning. `List<String?>` is a list whose elements can be empty: the list exists, and some places inside it are `null`. `List<String>?` is a type whose list itself can be empty: the list either exists or it does not, but if it exists, none of its elements is `null`. The two are independent, and both can be written at the same time.

The block below puts the two cases side by side. The size of the list with nullable elements is read directly, because the list itself cannot be null. With the type whose list itself is nullable, a `null` check is needed first; after the check, the same variable behaves as a `List<String>` and `size` is called directly.

```kotlin
// Main.kt — where the question mark goes
fun main() {
    val items: List<String?> = listOf("a", null)
    val maybeList: List<String>? = null
    println(items.size)
    println(maybeList == null)
    if (maybeList != null) {
        println(maybeList.size)
    } else {
        println("no-list")
    }
}
```

```
2
true
no-list
```

**TN3.** This measurement deals not with an empty list but with a list that is absent altogether (`null`); the behavior of an empty but existing list was not tried. The `List<String?>?` form, in which both `?` marks are written together, was not measured either.

## null with null

On the course's measurement axis, the real question is this: are two `null` values the same object? In Kotlin `null` is not an instance of a class but a single value. Every `null` is the same as every other; there is no identity that could tell one from another. As a result, referential equality (`===`) and structural equality (`==`) give the same answer for two `null` values. The rule stops at the first question: if the first value and its twin are the same object, the criterion is singleton and the remaining questions are not consulted.

The block below has four observations. In the first two, the first value and its twin are `null`, and the third value is filled in on one and `null` again on the other. The third and fourth observations use filled-in strings: the two strings are built separately with `buildString`, so two separate objects carrying the same content are obtained.

```kotlin
// Main.kt — null with null
fun main() {
    val nullFirst: String? = null
    val nullTwin: String? = null
    val full1 = buildString { append("cu"); append("be") }
    val full2 = buildString { append("cu"); append("be") }
    val checks = listOf(
        Criterion("null-null", nullFirst, nullTwin, "cube"),
        Criterion("null-null-null", nullFirst, nullTwin, null),
        Criterion("full-full", full1, full2, "another"),
        Criterion("full-null", full1, nullTwin, "another"),
    )
    for (check in checks) check.report()
    summarize(checks)
}
```

```
null-null: same-object=true, twin=true, other=false -> singleton
null-null-null: same-object=true, twin=true, other=true -> singleton
full-full: same-object=false, twin=true, other=false -> content
full-null: same-object=false, twin=false, other=false -> identity
singleton: 2
identity: 1
content: 1
partial-content: 0
```

The result reads off four lines. `null` with `null` is singleton. Two filled-in strings, with the same content but as separate objects, give the content criterion: `same-object=false`, `twin=true`, `other=false`. A filled-in string against `null` is identity: the twin is not equal. The criterion for the filled-in strings is the result of `String`'s own `equals`, not of any type redefined in this lesson.

There is a trap here: because the criterion for two `null` values came out singleton, you cannot conclude that everything compared with `null` is singleton. Only `null` with `null` is singleton. A filled-in value against `null` is identity, because the twin is not equal.

**TN4.** This measurement covers only `null` values of type `String?`; whether two `null` values of another type count as the same object was not tried. That the filled-in strings are separate objects was read from the `===` result when they were built with `buildString`; whether constant strings are shared as the same object was not tried.

## Ready-Made Functions on a Nullable Value

A `null` check is not always written with an `if`. The standard library offers a few ready-made functions whose receiver is `String?`: `toString`, `isNullOrEmpty`, and `orEmpty`. These functions do not throw an exception on a `null` receiver, because their definitions account from the start for the receiver possibly being `null`. The block below tries these functions with three values (`null`, an empty string, and a filled-in string). It also looks at what comes out when a string is concatenated with `null`.

```kotlin
// Main.kt — ready-made functions on a nullable receiver
fun main() {
    val missing: String? = null
    val empty: String? = ""
    val present: String? = "ab"
    println(missing.toString())
    println("[" + missing + "]")
    println(missing.isNullOrEmpty())
    println(empty.isNullOrEmpty())
    println(present.isNullOrEmpty())
    println(missing == null)
    println(missing.orEmpty().length)
}
```

```
null
[null]
true
true
false
true
0
```

Three things can be read from the output. First, `missing.toString()` throws no exception; on a `null` receiver it gives the string `"null"`. The `null` in the first line is therefore a string, not a `null` value. Second, concatenating a string with `null` works too, and the result is `[null]`: `String.plus` takes an `Any?` as its argument, so the compiler raises no objection; the side that can be null is the argument on the right. Third, `isNullOrEmpty` returns `true` for both `null` and the empty string, and `false` for the filled-in string; `orEmpty` converts `null` into the empty string, so the length comes out as `0`.

What these functions have in common is that the receiver type is `String?`. An ordinary `String` method cannot be called directly on this type, because the receiver may be absent. A common mistake is confusing the result of `toString` with a `null` value. The type of the result is always `String`; measuring the absence of a value by searching for the string `"null"` is wrong. Absence is asked about with `== null`. When code writes a string to a log, the two cases looking the same is the main reason the error is noticed late.

## Type Conversion and Null

Type conversion meets `null` as well. The `as` operator forces the conversion, `as?` gives `null` when it fails, and `is` only asks a question. The block tries all three on an `Any?` whose value is `null`; if the conversion fails, only the class name of the thrown exception is printed. In the last line, a filled-in value of the wrong type is converted with `as?`.

```kotlin
// Main.kt — as and as? on null
fun main() {
    val value: Any? = null
    println(value as? String)
    println(value is String)
    println(value is String?)
    try {
        val text = value as String
        println(text.length)
    } catch (e: Exception) {
        println(e::class.simpleName)
    }
    val other: Any? = 5
    println(other as? String)
}
```

```
null
false
true
NullPointerException
null
```

The three expressions show three different attitudes. When `as?` fails, it throws no exception and gives `null`; the result is `null` both for a `null` value and for a value of the wrong type. `is String` returns `false` for `null`, but `is String?` returns `true`, because the `null` value is a member of the `String?` type. The forcing `as String` throws a `NullPointerException` on a `null` value; the block catches it and prints only the class name.

The lesson here is that the two `null` outputs do not mean the same thing. The `null` in the first line comes from the value to be converted being absent in the first place. The `null` in the last line comes from the value being present but its type not fitting, and it is not possible to tell these two apart by looking at the output alone. When the distinction is needed, `value == null` is asked separately before the conversion. The forcing form of the conversion is written only where the type is known for certain; a wrong assumption comes back as an exception at run time, not at compile time.

## The Bounding Measurement

This lesson's rule is: the criterion is the answer to the first question. The rule asks four questions in order and stops at the first "yes". The first two lines of the earlier section already showed this, but the difference has to be seen explicitly: in the `null-null` line the third value is filled in (`"cube"`), and in the `null-null-null` line the third value is `null`. The equality of the third value (`other`) came out opposite in the two lines, while the criterion is singleton in both. Nothing between `other=false` and `other=true` changes the criterion; this is because `Criterion` stops at the first question, and the third value adds no information to the criterion.

This shows where the limit lies. The `other` column is printed, because `Criterion` computes the third comparison in every case; but in a singleton criterion that column is not read. Saying that a value is singleton does not tell you how many values are equal to it. It says only that the twin is the same object.

The same limit can be read from the opposite direction. If the first value were `null` and its twin a filled-in string, `same-object=false` and `twin=false` would come out: the criterion would be identity. So starting with `null` does not make the criterion singleton; for it to be singleton, the twin must be `null` as well. What determines the criterion is not the type of the first value but whether the first value and its twin are the same object.

**TN5.** The bounding measurement was made with two `null` values and a filled-in or `null` third value; the ordering in which the first value is filled in and the twin is `null` was not given a separate output, and was only inferred from the identity result on the `full-null` line. `Criterion`'s rule was not tried with other orderings.

## Summary

- `String` and `String?` are separate types: assigning `null` to a `String` and asking for `length` directly on a `String?` do not compile.
- A null check inside an `if` narrows the variable's type to `String` in that branch; this is a smart cast, and no conversion is written.
- `filterNotNull` turns a `List<String?>` into a `List<String>` and discards the `null` elements.
- Two `null` values are the same object and the criterion is singleton; two separate filled-in strings give content, and a filled-in string against `null` gives identity.
- The rule stops at the first question: in the first two lines the equality of the third value came out opposite while the criterion stayed the same.
- This lesson's criterion: **singleton**. `null` with `null` is the same object.

## Next Step

In this lesson we saw `null` in the type and checked it with `if`. Every `null` check asks for a separate `if`, and the code grows longer as the checks line up one after another. The next lesson measures the operators that do the same job by a shorter route: where a `?.` safe call chain stops when it sees `null`, when the `?:` Elvis operator evaluates its right side, and which exception is thrown when a not-null assertion turns out wrong.
