article banner

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.
var person: Person // COMPILATION ERROR, // the property needs to be initialized
  • A regular type does not accept the null value.
var person: Person = null // COMPILATION ERROR, // Person is not a nullable type, and cannot be `null`
  • To specify a nullable type, you need to end a regular type with a question mark (?).
var person: Person? = null // OK
  • Nullable values cannot be used directly. They must be used safely or cast first (using one of the tools presented later in this chapter).
person.name // COMPILATION ERROR, // person type is nullable, so we cannot use it directly

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 returns null,
  • if a value is not null, it works like a regular call.
class User(val name: String) { fun cheer() { println("Hello, my name is $name") } } var user: User? = null fun main() { user?.cheer() // (does nothing) println(user?.name) // null user = User("Cookie") user?.cheer() // Hello, my name is Cookie println(user?.name) // Cookie }

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.

class User(val name: String) { fun cheer() { println("Hello, my name is $name") } } var user: User? = null fun main() { // println(user?.name.length) // ILLEGAL println(user?.name?.length) // null user = User("Cookie") // println(user?.name.length) // ILLEGAL println(user?.name?.length) // 6 }

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 !!.

class User(val name: String) { fun cheer() { println("Hello, my name is $name") } } var user: User? = User("Cookie") fun main() { println(user!!.name.length) // 6 user = null println(user!!.name.length) // throws NullPointerException }

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 throws IllegalArgumentException if this value is null. Otherwise, it returns this value as non-nullable.
  • checkNotNull, which accepts a nullable value as an argument and throws IllegalStateException if this value is null. Otherwise, it returns this value as non-nullable.
private val connections = ConcurrentHashMap<String, Connection>() fun sendData(dataWrapped: Wrapper<Data>) { val data = requireNotNull(dataWrapped.data) val connection = checkNotNull(connections["db"]) connection.send(data) }

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.

fun printLengthIfNotNull(str: String?) { if (str != null) { println(str.length) // str smart-casted to String } }

Smart-casting also works when we use return or throw if a value is not null.

fun printLengthIfNotNull(str: String?) { if (str == null) return println(str.length) // str smart-casted to String }
fun printLengthIfNotNullOrThrow(str: String?) { if (str == null) throw Error() println(str.length) // str smart-casted to String }

Smart-casting is quite smart and works in different cases, such as after && and || in logical expressions.

fun printLengthIfNotNull(str: String?) { if (str != null && str.length > 0) { // str in expression above smart-casted to String // ... } }
fun printLengthIfNotNull(str: String?) { if (str == null || str.length == 0) { // str in expression above smart-casted to String // ... } }
fun printLengthIfNotNullOrThrow(str: String?) { requireNotNull(str) // str smart-casted to String println(str.length) }

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.

fun main() { println("A" ?: "B") // A println(null ?: "B") // B println("A" ?: null) // A println(null ?: null) // null }

We can use the Elvis operator to provide a default value for nullable values.

class User(val name: String) fun printName(user: User?) { val name: String = user?.name ?: "default" println(name) } fun main() { printName(User("Cookie")) // Cookie printName(null) // default }

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 not null. Otherwise it returns an empty string.
  • isNullOrEmpty returns true if the value is null or empty. Otherwise, it returns false.
  • isNullOrBlank returns true if the value is null or blank. Otherwise, it returns false.
fun check(str: String?) { println("The value: \"$str\"") println("The value or empty: \"${str.orEmpty()}\"") println("Is it null or empty? " + str.isNullOrEmpty()) println("Is it null or blank? " + str.isNullOrBlank()) } fun main() { check("ABC") // The value: "ABC" // The value or empty: "ABC" // Is it null or empty? false // Is it null or blank? false check(null) // The value: "null" // The value or empty: "" // Is it null or empty? true // Is it null or blank? true check("") // The value: "" // The value or empty: "" // Is it null or empty? true // Is it null or blank? true check(" ") // The value: " " // The value or empty: " " // Is it null or empty? false // Is it null or blank? true }

Kotlin stdlib also includes similar functions for nullable lists:

  • orEmpty returns the value if it is not null. Otherwise, it returns an empty list .
  • isNullOrEmpty returns true, returns true if the value is null or empty. Otherwise, it returns false.
fun check(list: List<Int>?) { println("The list: \"$list\"") println("The list or empty: \"${list.orEmpty()}\"") println("Is it null or empty? " + list.isNullOrEmpty()) } fun main() { check(listOf(1, 2, 3)) // The list: "[1, 2, 3]" // The list or empty: "[1, 2, 3]" // Is it null or empty? false check(null) // The list: "null" // The list or empty: "[]" // Is it null or empty? true check(listOf()) // The list: "[]" // The list or empty: "[]" // Is it null or empty? true }

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.

@AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var presenter: MainPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) presenter.onCreate() } } class UserServiceTest { lateinit var userRepository: InMemoryUserRepository lateinit var userService: UserService @Before fun setup() { userRepository = InMemoryUserRepository() userService = UserService(userRepository) } @Test fun `should register new user`() { // when userService.registerUser(aRegisterUserRequest) // then userRepository.hasUserId(aRegisterUserRequest.id) // ... } }

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.

lateinit var text: String fun main() { println(text) // RUNTIME ERROR! // lateinit property text has not been initialized }

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.

lateinit var text: String private fun printIfInitialized() { if (::text.isInitialized) { println(text) } else { println("Not initialized") } } fun main() { printIfInitialized() // Not initialized text = "ABC" printIfInitialized() // ABC }

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.

0:

Some research confirms this: for example, data collected by OverOps confirms that NullPointerException is the most common exception in 70% of production environments.

1:

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.

2:

There is more on using nullability in the book Effective Kotlin.

3:

These are extension functions, which we will discuss in the chapter Extensions.

4:

There is more about exceptions, IllegalArgumentException and IllegalStateException in the chapter Exceptions.

5:

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.