Skip to main content

Problem Solving with Algorithms and Data Structures using Kotlin The Interactive Edition

Section 1.16 Null Safety

Kotlin introduces a relatively new feature into the language called null safety, which helps to prevent a significant source of bugs. Kotlin achieves this by causing an error while compiling if a program potentially has bugs in it relating to null safety. This requires the programmer to make sure that the program is safe from causing a null-safety-related bug before it starts running. Older languages such as Java, C++, and Python (sort of) are not null-safe, which means that if you have programmed in any of these languages, you might be in the habit of writing unsafe code that would not compile in Kotlin. We’ll look in this section at the nature of the problem that Kotlin fixes, and how to write correct null-safe code in Kotlin.

Subsection 1.16.1 The Null Safety Problem

In this section, we’ll show some code that illustrates the problem that null safety solves. We’ll focus on examples in Java, but even if you have only previously programmed in Python or some other language, the code will be short and straightforward enough that you can hopefully see the key principles. The goal here is just to motivate the problem that Kotlin fixes; in the next section, we’ll go back to looking at Kotlin code.
This portion of a Java program tries to convert a string to upper case and print it, but the variable text has been assigned a value of null, which means that we do not wish to assign it to an actual string object. (In Python, None serves a similar purpose.) This program compiles without any problems at all, but when we run it, it results in the error messsage shown:
Listing 1.16.1. Java snippet converting a string to upper case
String text = null;
System.out.println(text.toUpperCase());
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.toUpperCase()" because "<local1>" is null

Aside: Similar snippet in Python, with similar error.

This NullPointerException indicates that the variable text is a null pointer, meaning that it does not point to an actual string object. When we try to run the method toUpperCase on it, there is no string object to be found, so the method can’t run.
Let’s now modify the program so that the error only happens very rarely. This modification is designed to be brief and so it is somewhat silly, but let’s go with it. This program only sets the variable text to null if a random number generated between 0 and 1 happens to be less than 0.001. In other words, this program would only cause an error, on average, once in a thousand runs.
Listing 1.16.2. Java snippet converting a string to upper case, but is null only rarely
String text = "hello";
if (Math.random() < 0.001) {
    text = null;
}
System.out.println(text.toUpperCase());

Aside: Similar snippet in Python.

This Java program compiles with no problems, and runs without error most of the time. It is quite possible that the programmer never sees the program crash. And yet, if a large number of people end up using this program, a small number of them will see this error. Though this example is silly, it is a proxy for real-world cases where null might happen sometimes, and the programmer forgot to handle it appropriately. These bugs are entirely avoidable if the programmer plans for the possiblity of a null value happening. For example, here is how the problem of the rare-but-possible null value could be fixed in our silly program:
Listing 1.16.3. Java snippet converting a string to upper case, but is null only rarely
String text = "hello";
if (Math.random() < 0.001) {
    text = null;
}

if (text != null) {
    System.out.println(text.toUpperCase());
} else {
    System.out.println("Text is null.");
}

Aside: Similar fixed snippet in Python.

In this fixed version of the program, we first check that the variable text is not null before running the upperCase method. This is the trick to preventing NullPointerExceptions from happening: if a variable potentially might be null, a check should be made first before running code that depends on it not being null.
In languages such as Java, Python, C, and many others, the core language itself does not offer any built-in protection to help the programmer avoid this problem. Newer languages such as Kotlin (and some new libraries that are being added to old languages) offer support for this. In the following sections, we’ll look at how Kotlin handles this issue.

Subsection 1.16.2 How Kotlin Handles Null Values

Let’s convert the first broken program above to Kotlin, and see what happens. Our first attempt might look like this:
Listing 1.16.4. First attempt at converting program to Kotlin
fun main() {
    var text: String = null
    println(text.uppercase())
}
In line 2 we specified the type of the variable text as String. Usually in Kotlin we leave the type information out because Kotlin is able to infer what that type is based on the value we assign it, but in this case, null isn’t enough information for Kotlin to be able to guess the type. When we compile this code, however, we get this error message on line 2:
Null cannot be a value of a non-null type 'String'.
This illustrates an important difference in how Kotlin handles types compared to other languages you may have previously used. In order to solve the null safety problem, Kotlin needs to know when a variable might be null. We do this in Kotlin by placing a question mark after the name of the type. In other words, instead of indicating that the variable text is of type String, we indicate that it is of type String?. The question mark is used to indicate that this a different type that is almost the same as String, but null values are also allowed. String? is referred to as a nullable type.
So let’s make that change to the above program:
Listing 1.16.5. Kotlin program with nullable type
fun main() {
    var text: String? = null
    println(text.uppercase())
}
This resolves the problem that we had on line 2, but now we get a compilation error on line 3. Kotlin has identified that we are calling the method uppercase on a variable that might be null.

Aside: Deciphering the actual error message.

We can resolve the error we get by checking for null first:
Listing 1.16.6. Kotlin program, fixed
fun main() {
    var text: String? = null
    if (text != null) {
        println(text.uppercase())
    } else {
        println("Text is null.")
    }
}
The above program compiles without error.
Here are the important details you need to know about how Kotlin handles null values:
  • Kotlin will not compile a program that potentially assigns null to a variable unless the variable has a nullable type (i.e., has a question mark).
  • Kotlin will not compile a program that tries to call a method on a variable with a nullable type, unless the program has first safely checked that the type is not null.

Subsection 1.16.3 Approaches for Resolving Null Problems

As you write code in Kotlin, you will sometimes run into the problem that your code won’t compile because you have not appropriately handled nulls. There are multiple ways of handling it. In this section, we’ll consider some approaches.
The simplest way to resolve a null safety error is to use the approach that we used in the code above. Use an if to see if the value is null, and handle accordingly.
One place where this does not work is if the variable is an instance variable associated with an object. Here’s a simple example:
Listing 1.16.7. Null issue with instance variable
class Wrapper(var text: String?) {
    fun printUpperText() {
        if (text != null) {
            println(text.uppercase())
        } else {
            println("Text is null.")
        }
    }
}
In the above program, the class Wrapper has an instance variable text, that is of type String?. text therefore potentially might have a value of null, and hence we need to check if it is null before calling methods on it, as we do in the code above. However, even with the if in our code, we still get an error on line 4 indicating that we are inappropriately calling a method on something that might be null. This seems strange; we’re checking that it’s not null. The problem is that text is an instance variable associated with an object in the class, and that means it’s theoretically possible that in a parallel multithreaded program, some other thread in the program has access to this object and changes text to null while we are in between lines 3 and 4. In this textbook, we do no parallel coding at all, and so this is an impossibility with the kind of code we’ll be writing. But this is a real possibility for Kotlin programs in general, so Kotlin insists that this is a problem.
So what are we to do? One recommended approach here is to make a copy of the instance variable to a local variable, and then check that:
Listing 1.16.8. Null issue resolved with instance variable copy
class Wrapper(var text: String?) {
    fun printUpperText() {
        val localText = text
        if (localText != null) {
            println(localText.uppercase())
        } else {
            println("Text is null.")
        }
    }
}
The variable localText is defined locally to the method printUpperText, which means that no other part of the program can access it. There is therefore no risk of someone else accessing the variable in between the check on line 4 and its usage on line 5. We’ll often use this approach in sample code within this book.
Another common problem that occurs is when we write code for which we know that a variable cannot be null due to specific program logic, but Kotlin is unable to see that this is the case. Here’s an example:
Listing 1.16.9. Null issue due to complicated logic
import kotlin.random.Random
fun main() {
    var text: String? = null
    val number = Random.nextInt(100)
    if (number % 2 == 0) {
        text = "even"
    }
    if (number % 2 == 1) {
        text = "odd"
    }
    println(text.uppercase())
}
This program initializes a variable text to null, then picks a random integer. If the integer is even, then text is set to the string "even"; if the integer is odd, then text is set to the string "odd". All integers must be even or odd, so line 11 should be perfectly safe, but Kotlin still flags line 11 as an error. Kotlin can only observe that there are two if statements (lines 5-7, and lines 8-10), and the Kotlin null safety checker is not smart enough to determine that one of those two conditions is always true.
This program is contrived to illustrate a point and it is likely best fixed by changing the second if to an else. But in general, the problem remains that a programmer might write program logic that makes a null value impossible, but Kotlin may not be able to conclude that appropriately. A common fix for this situation is the not-null assertion operator, which appears as !!. This requires just a single fix to line 11, which would appear as:
Listing 1.16.10. Not-null assertion operator
println(text!!.uppercase())
The !! operator communicates to the Kotlin null safety checker that it should ignore the possibility of null for this method call when compiling the code. This means that if the variable really is null, then the program would result in a NullPointerException when it is executed. !! is a useful tool, but it is very easy to abuse it. You might find it tempting to drop in !! every time you get a null safety error. Don’t do this! If you do, you will likely end up with NullPointerExceptions when your program runs, which are much harder to diagnose. The !! operator should be used sparingly, and only when you are 100% confident that your program logic is such that a null value is impossible. If the reason for that is not immediately obvious, you should likely document why in your code.

Subsection 1.16.4 More Approaches for Resolving Null Problems

Kotlin has a wide variety of approaches for handling null problems, many of which lie outside the scope of this book. There is one in particular that we recommend Kotlin beginners avoid using, because it is likely more confusing than helpful and the other approaches above are just as effective. However, this approach is present in many online examples, so the reader should at least be aware of its existence.
This particular approach makes use of the "null safety" operator, which is represented by a question mark right before making a method call. For example, from our program in Listing 1.16.9, Kotlin was unhappy with this code, because of the possibility that text might be null:
println(text.uppercase())
To fix this with the null safety operator, we would insert a question mark:
println(text?.uppercase())
This code will compile, and run, without error. However, what this actually does is not obvious. The ? operator checks to see if the variable before it is null. If it is not, then Kotlin continues and executes the method call that follows. On the other hand, if the variable is null, Kotlin aborts the rest of the expression entirely, and just returns null. So in the above example, if text actually were null, the program would simply print null. This is almost never what you want to actually happen.
There are ways of using the null safety operator in a smart way to avoid these problems. One of them is to use the ?: operator, which is sometimes called the "Elvis operator" because when looked at sideways it perhaps resembles the singer Elvis Presley. Here is how it might be used in the above example:
println(text?.uppercase() ?: "Text is null")
The ?: operator tests to see if the expression to its left is null, and if it is, it then returns the expression to the right. So here is how the above code would work in its entirety:
  • The null safety operator ? checks to see if text is null.
  • If it is not null, the uppercase method is run, and the result of that is printed.
  • If text is null, then text?.uppercase() just returns null, which feeds into the Elvis operator.
  • The Elvis operator returns "Text is null", which is then printed.
This code is competely legitimate Kotlin, but for the types of programs we write in this book this aproach is generally more confusing than helpful. We tend to avoid it in this book.
You have attempted of activities on this page.