Nullability in Kotlin
This is a chapter from the book Kotlin Essentials. You can find it on LeanPub or Amazon. It is also available as a course.
Kotlin started as a remedy to Java problems, and the biggest problem in Java is nullability. In Java, like in many other languages, every variable is nullable, so it might have the null
value. Every call on a null
value leads to the famous NullPointerException
(NPE). This is the #1 exception in most Java projects0. It is so common that it is often referred to as “the billion dollar mistake” after the famous speech by Sir Charles Antony Richard Hoare, where he said: “I call it my billion-dollar mistake. It was the invention of the null reference in 1965… This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years”.
One of Kotlin's priorities was to solve this problem finally, and it achieved this perfectly. The mechanisms introduced in Kotlin are so effective that seeing NullPointerException
thrown from Kotlin code is extremely rare. The null
value stopped being a problem, and Kotlin developers are no longer scared of it. It has become our friend.
So, how does nullability work in Kotlin? Everything is based on a few rules:
- Every property needs to have an explicit value. There is no such thing as an implicit
null
value.
- A regular type does not accept the
null
value.
- To specify a nullable type, you need to end a regular type with a question mark (
?
).
- Nullable values cannot be used directly. They must be used safely or cast first (using one of the tools presented later in this chapter).
Thanks to all these mechanisms, we always know what can be null
and what cannot. We use nullability only when we need it - when there is a reason to. In such cases, users are forced to explicitly handle this nullability. In all other cases, there is no need to do this. This is a perfect solution, but good tools are needed to deal with nullability in a way that’s convenient for developers.
Kotlin supports a variety of ways of using a nullable value, including safe calls, not-null assertions, smart-casting, or the Elvis operator. Let's discuss these one by one.
Safe calls
The simplest way to call a method or a property on a nullable value is with a safe call, which is a question mark and a dot (?.
) instead of just a regular dot (.
). Safe calls work as follows:
- if a value is
null
, it does nothing and returnsnull
, - if a value is not
null
, it works like a regular call.
Notice that the result of a safe call is always a nullable type because a safe call returns null
when it is called on a null
. This means that nullability propagates. If you need to find out the length of a user's name, calling user?.name.length
will not compile. Even though name
is not nullable, the result of user?.name
is String?
, so we need to use a safe call again: user?.name?.length
.
Not-null assertion
When we don’t expect a null
value, and we want to throw an exception if one occurs, we can use the not-null assertion !!
.
This is not a very safe option because if we are wrong and a null
value is where we don't expect it, this leads to a NullPointerException
. Sometimes we want to throw an exception to ensure that there are no situations where null
is used, but we generally prefer to throw a more meaningful exception4. For this, the most popular options are:
requireNotNull
, which accepts a nullable value as an argument and throwsIllegalArgumentException
if this value is null. Otherwise, it returns this value as non-nullable.checkNotNull
, which accepts a nullable value as an argument and throwsIllegalStateException
if this value is null. Otherwise, it returns this value as non-nullable.
Smart-casting
Smart-casting also works for nullability. Therefore, in the scope of a non-nullability check, a nullable type is cast to a non-nullable type.
Smart-casting also works when we use return
or throw
if a value is not null
.
Smart-casting is quite smart and works in different cases, such as after &&
and ||
in logical expressions.
Smart-casting works in the code above thanks to a feature called contracts, which is explained in the book Advanced Kotlin.
The Elvis operator
The last special Kotlin feature that is used to support nullability is the Elvis operator ?:
. Yes, it is a question mark and a colon. It is called the Elvis operator because it looks like Elvis Presley (with his characteristic hair), looking at us from behind a wall, so we can only see his hair and eyes.
It is placed between two values. If the value on the left side of the Elvis operator is not null
, we use the nullable value that results from the Elvis operator. If the left side is null
, then the right side is returned.
We can use the Elvis operator to provide a default value for nullable values.
Extensions on nullable types
Regular functions cannot be called on nullable variables. However, there is a special kind of function that can be defined such that it can be called on nullable variables3. Thanks to this, Kotlin stdlib defines the following functions that can be called on String?
:
orEmpty
returns the value if it is notnull
. Otherwise it returns an empty string.isNullOrEmpty
returnstrue
if the value isnull
or empty. Otherwise, it returnsfalse
.isNullOrBlank
returnstrue
if the value isnull
or blank. Otherwise, it returnsfalse
.
Kotlin stdlib also includes similar functions for nullable lists:
orEmpty
returns the value if it is notnull
. Otherwise, it returns an empty list .isNullOrEmpty
returnstrue
, returnstrue
if the value isnull
or empty. Otherwise, it returnsfalse
.
These functions help us operate on nullable values.
null
is our friend
Nullability was a source of pain in many languages like Java, where every object can be nullable. As a result, people started avoiding nullability. As a result, you can find suggestions like "Item 43. Return empty arrays or collections, not nulls" from Effective Java 2nd Edition by Joshua Bloch. Such practices make literally no sense in Kotlin, where we have a proper nullability system and should not be worried about null
values. In Kotlin, we treat null
as our friend, not as a mistake1. Consider the getUsers
function. There is an essential difference between returning an empty list and null
. An empty list should be interpreted as "the result is an empty list of users because none are available". The null
result should be interpreted as "could not produce the result, and the list of users remains unknown". Forget about outdated practices regarding nullability. The null
value is our friend in Kotlin2.
lateinit
There are situations where we want to keep a property type not nullable, and yet we cannot specify its value during object creation. Consider properties whose value is injected by a dependency injection framework, or consider properties that are created for every test in the setup phase. Making such properties nullable would lead to inconvenient usage: you would use a not-null assertion even though you know that the value cannot be null
because it will surely be initialized before usage. For such situations, Kotlin creators introduced the lateinit
property. Such properties have non-nullable types, and cannot be initialized during creation.
When we use a lateinit property, we must set its value before its first use. If we don't, the program throws an UninitializedPropertyAccessException
at runtime.
You can always check if a property has been initialized using the isInitialized
property on its reference. To reference a property, use two colons and a property name5.
Summary
Kotlin offers powerful nullability support that turns nullability from scary and tricky into useful and safe. This is supported by the type system, which separates what is nullable or not nullable. Variables that are nullable must be used safely; for this, we can use safe-calls, not-null assertions, smart-casting, or the Elvis operator. Now, let's finally move on to classes. We’ve used them many times already, but we finally have everything we need to describe them well.
Some research confirms this: for example, data collected by OverOps confirms that NullPointerException
is the most common exception in 70% of production environments.
See the "Null is your friend, not a mistake" (link kt.academy/l/re-null) article by Roman Elizarov, current Project Lead for the Kotlin Programming Language.
There is more on using nullability in the book Effective Kotlin.
These are extension functions, which we will discuss in the chapter Extensions.
There is more about exceptions, IllegalArgumentException
and IllegalStateException
in the chapter Exceptions.
To reference a property from another object, we need to start with the object before we use ::
and the property name. There is more about referencing properties in the Advanced Kotlin book.