Kotlin Null Safety

Background

java.lang.NullPointerException is an exception Java developers often encounter. It is thrown when code tries to reference an object but that object is null. For example, if null is assigned to a variable of type String and the length method is called on that variable, a NullPointerException (NPE) occurs.

null is used when there is no value. For example, when a user with a specified ID does not exist, a method such as findUserById may return null instead of returning an instance of the User class.

From this perspective, null can be convenient. However, because of null, developers encounter the NPE, an exception they would rather not see. If you check null properly, for example with an if condition that confirms the value is not null, the problem can be avoided. So why does it still happen?

For return values from methods that you know do not return null, it is common not to perform a null check. The important point is that even if a method is intended by specification not to return null, Java code cannot guarantee it. In Java, variables and method return values can become null anywhere at any time. As a result, code that needs null checks and code that does not need them are mixed together, which can accidentally lead to NPEs.

Ways to handle null in Java

There are several ways to distinguish between values that may be null and values that are not null.

Communicating through method signatures

This is a primitive approach. A method that may return null is named in a way that warns developers. For example, a method might be named getNameOrNull. By reading the method name, you can notice that it may return null.

Static analysis tools

Another approach is to add annotations to methods and let static analysis tools point out issues. If a getName method may return null, write something like @Nullable String getName() {...}. If it does not return null, write @NotNull String getName() {...}.

Expressing it with types

This approach uses a newly defined type instead of null to express a value that may not exist. A concrete example is the java.util.Optional class introduced in Java SE 8. If there is no value, return an object from Optional#empty; if a value exists, pass that value to Optional#of and wrap it. The Optional type provides many useful, easy-to-use methods for distinguishing values that may not exist from values that definitely exist.

Kotlin null safety

Using static analysis tools and Optional is a good approach. However, in Java, variables and method return values can still be null anywhere at any time. The example below uses @NonNull and Optional, but an NPE still occurs.

Example: null can still occur.

import org.springframework.lang.NonNull;

import java.util.Optional;

public class Foo {
    // Java code.
    @NonNull
    Optional<String> getName() {
        return null;
    }

    public static void main(String[] args) {
        Foo foo = new Foo();
        foo.getName().toString();
    }
}

Output:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.toString()" because the return value of "Foo.getName()" is null
	at Foo.main(Foo.java:15)

This is why Kotlin’s null-safety feature exists. Kotlin provides a built-in language feature that distinguishes values that can be null (Nullable) from values that cannot be null (NotNull).

Unlike using Optional, Kotlin does not require creating new wrapper instances or additional GC work, so it avoids that overhead. This is especially useful in resource-constrained environments such as Android.

Basic usage

Earlier, variables were initialized and reassigned. Variable a is of type String. The type is written explicitly here, but it can be omitted. Because it is declared as a mutable variable with the var keyword, "Goodbye" can be assigned again. However, a compile error occurs on the next line where null is assigned. Variable a is declared as NotNull, so the compiler does not allow null to be assigned. This means you can use variable a with confidence that it is always not null.

Example: NotNull variable

var a: String = "Hello"
a = "Goodbye"
a = null // A compile error occurs here.

So how do you declare a Nullable variable that can be assigned null? It is simple: add ? after the type. In the example below, the type of variable b is String?. This means “a String type that can be assigned null.” null is assigned on the second line, and the code compiles. In this case, the question mark (?) cannot be omitted from the type of b, because if it is omitted, "Hello" is inferred as String instead of String?.

Example: Nullable variable

var b: String? = "Hello"
b = null

Kotlin clearly distinguishes between Nullable and NotNull. Since null cannot enter a NotNull variable, referencing it is safe from NPEs. Then does an NPE occur for a Nullable variable because it can contain null? Let’s intentionally try to cause an NPE.

Example: Try to cause an NPE

val str: String? = null
str.length // A compile error occurs here.

The String? variable str is initialized with null, and the code tries to call the length property on it to cause an NPE. In reality, however, a compile error occurs. Kotlin does not want to let you cause an NPE, so it reports an error at compile time for operations that may cause one. References to methods and properties on Nullable values are prohibited.

In practice, if members of Nullable values could never be accessed, Nullable would not be usable. There are, of course, ways to access them. One method is to check for null.

If you check whether str is null as shown below, str can be treated as NotNull within the guaranteed scope, the if block. If this code runs when "Hello" is assigned to str, it prints "5".

Example: Checking null makes it NotNull

if(str != null) {
  println(str.length)
}

Convenient Nullable features

The previous section is enough knowledge to use Kotlin’s Nullable types. However, writing null checks repeatedly is tedious and verbose, so Kotlin provides features that make Nullable values easier to use.

Safe calls

Earlier, a null check was used to call a method on a Nullable value. A safe call provides a concise way to write this.

In the example below, the length property of a variable str of type String? is called safely. The difference from a normal method call is that a question mark (?) is placed before the dot (.). This becomes a safe call, allowing a Nullable method or property to be called safely, without causing an NPE.

Example: Safe call

val str: String? =  null

// Safe-call style
val length1: Int? = str?.length

// null-check style
val length2: Int? = if(str != null) str.length else null

If str is null, the method is not called and null is returned. The first safe-call style and the second null-check style are equivalent.

Safe calls are especially effective when you want to write method chaining. If you write foo()?.bar()?.baz(), even if a value in the middle is null, the safe calls are chained and the final result is simply null.

Example: Method chaining with safe calls

// Safe-call style
val result1 = foo()?.bar()?.baz()

// null-check style
val foo = foo()
val result2 = if(foo != null) {
    val bar = foo.bar()
    if(bar != null) bar.baz() else null
} else {
    null
}

Default values

You can easily specify a value to use when the original value is null. This is done with the Elvis operator (?:) after a Nullable expression. In the example below, s?.length returns the string length or null through a safe call. If null is returned, the default value is set to 0. If the result is not null, the value returned by the method call is assigned.

Example: Default value

val s: String? = "Hello"

// Using the Elvis operator
val length1: Int = s?.length ?: 0

// null-check style
val length: Int? = s?.length
val length2: Int = if(length != null) length else 0

The not-null assertion operator !!

The last feature introduced here is the !! operator.

The !! operator forcibly converts a Nullable variable to NotNull. In the example below, the variable str of type String? is forcibly converted to String by the !! operator.

Example: Forcibly converting to NotNull

val str: String? = "Hello"
println(str!!.length) // => 5

This example runs without any problem. However, the next example causes a runtime exception. If you use the !! operator when the value is null, a NullPointerException occurs.

Example: Runtime exception

val str: String? = null
println(str!!.length)

If the value is null, an exception occurs. In the end, this is the same as before. If you want to use the !! operator, first consider a null check, a safe call, or a default value. Use !! only when unavoidable. It is also a good idea to leave a comment explaining why it was used.

Now look at another case where you may want to use the !! operator. Suppose you have elements that allow null, and you want a function that returns a new list (List<T>) containing only NotNull elements without keeping null values. It can be implemented as follows.

Example: Implementing filterNotNull

fun <T> filterNotNull(list: List<T?>): List<T> =
    list.filter { it != null }
        .map { it!! }

fun main() {
    val a: List<String?> = listOf("kimkc", null, "devkuma")
    val b: List<String> = filterNotNull(a)
    println(b) // => ["kimkc", "devkuma"]
}

list.filter { it != null } filters elements that are not null. However, the list type is still List<T?>. Therefore, map { it!! } forcibly converts each element type from T? to T.

In practice, you can implement this without using the !! operator, and filterNotNull is provided as a standard collection method.

Example: Using the standard filterNotNull collection method

val a: List<String?> = listOf("kimkc", null, "devkuma")
val b: List<String> = a.filterNotNull()
println(b) // => ["kimkc", "devkuma"]

Standard extension function let

Kotlin’s standard library provides an extension function named let for all types. Strictly speaking, it is declared as public inline, but that detail is omitted here for convenience.

Code: Standard extension function let

fun <T, R> T.let(f: (T) -> R): R = f(this)

let applies the function f passed as an argument to the receiver object (this). Here is an example.

Example: Using let

5.let {
  println(it * 3) // => 15
}

The only argument passed to the function literal, the implicit variable it, is the same object as the receiver of let. In this case, it is 5.

How does this simple function help? It is useful when applying a function that takes a NotNull argument to a Nullable value. Let’s look at it concretely.

In the example below, the success function takes a NotNull Int argument. You want to pass the Nullable variable a as an argument to this function, but you cannot because Nullable and NotNull are different. success is a function, not an Int method or extension function, so a safe call cannot be used. In this case, you must use an if statement to check for null and safely pass the Nullable value as NotNull.

Example: Applying a function that receives NotNull

fun success(n: Int): Int = n + 1

// Nullable variable
val a: Int? = 3
val b: Int? =
    if (a != null) success(a)
    else null

println(b) // => 4

In this situation, use let. Since let is an extension function for all types, you can use a safe call like a?.let {...}. The argument accepted by the function passed to let (the f in “Code: Standard extension function let”) is a NotNull value of the same type as the receiver. This works because when the receiver is null, the let extension function is not called.

By using let, the part that applies success can be rewritten as follows.

Example: Cleaner with let

val b: Int? = a?.let { success(it) }

It can be written even more concisely with a function reference.

Example: Cleaner with a function reference

val b: Int? = a?.let(::success)

For developers already familiar with Optional in Java, it may be helpful to think of let as playing the roles of map, flatMap, and ifPresent.

References