1. Previous: Chapter 5
  2. Next: Chapter 7
Kotlin: An Illustrated Guide • Chapter 6

Nulls and Null Safety

Chapter cover image
“Now serving number 12648430!”

So far in this book, every time that we created a variable, whether it was a String, an Int, or a Boolean, we assigned a value to it. There are times, though, when we need to create a variable that might not actually hold a value!

This brings us to the exciting topic of nulls!

Introduction to Nulls

James has set up a coffee stand downtown, and he’s ready to start sharing his fine brew! After handing out each cup, he asks the guest to review the coffee, so that he can share the ratings with others who might be interested, hoping that it will encourage others to try his coffee. To the right is an example of the review form that he provides to his guests.

A comment card for a guest to enter their name, a comment, and a star rating. JAMES' COFFEE: STARS: 0   1   2   3   4   5 NAME: COMMENT:

Since he wanted to keep track of the ratings in a Kotlin program, he wrote this simple class.

class CoffeeReview(
    val name: String,
    val comment: String,
    val stars: Int
)

The name property represents the name of the person who is reviewing his coffee, the comment property is used for any comment that they want to share about it, and the stars property holds the star rating—the number of stars that they give the coffee, between 0 and 5.

The first three guests of the day have filled out the review cards. Here’s how they rated his coffee!

Three comment cards - Sarah, 'Loved the coffee!', 5 stars; Toby, 'Pretty good!', 4 stars; Lucy, 'Will buy this again!', did not specify any stars. JAMES' COFFEE: STARS: 0   1   2   3   4   5 NAME: COMMENT: JAMES' COFFEE: STARS: 0   1   2   3   4   5 NAME: COMMENT: JAMES' COFFEE: STARS: 0   1   2   3   4   5 NAME: COMMENT:

James instantiated some CoffeeReview objects to record the three reviews that he received. He started with the first two.

val saraReview = CoffeeReview("Sara", "Loved the coffee!", 5)
val tobyReview = CoffeeReview("Toby", "Pretty good!", 4)

When he got to Lucy’s review, though, he noticed that she forgot to leave a star rating. “That’s okay,” he said to himself, “I’ll just use the number zero since she didn’t mark any stars.”

val lucyReview = CoffeeReview("Lucy", "Will buy this again!", 0)

He was ready to show the reviews on a screen, so he wrote a simple function, and called it with each of the reviews that he received.

fun printReview(review: CoffeeReview) =
    println("${review.name} gave it ${review.stars} stars!")

println("Latest coffee reviews")
println("---------------------")
printReview(saraReview)
printReview(tobyReview)
printReview(lucyReview)

This is what showed on the screen:

Latest coffee reviews
---------------------
Sara gave it 5 stars!
Toby gave it 4 stars!
Lucy gave it 0 stars!

He thought that his solution would work well, but when the guests saw the reviews, they thought, “Wow, if Lucy didn’t like the coffee, maybe it’s not good. I’ll go somewhere else.”

James realized that a zero-star rating is not the same thing as having no star rating.

Two comment cards - one where the guest circled the zero for the star rating, and one where the guest didn't circle any number for the star rating. JAMES' COFFEE: STARS: 0   1   2   3   4   5 NAME: COMMENT: JAMES' COFFEE: STARS: 0   1   2   3   4   5 NAME: COMMENT: Zero-Star Rating No Star Rating

When someone doesn’t leave a star rating, then James doesn’t want to show a zero-star rating on the screen. Instead, he needs a way to tell Kotlin that the guest didn’t leave a star rating at all. How can he do that?

Present and Absent Values

As you might recall from Chapter 1, a variable is like a bucket that holds a value.

In all the code that we’ve written so far, we’ve created variables that contain a value. In other words, we’ve always had something inside that bucket. For example, a stars bucket contains an Int, like the number 5.

A bucket with the label 'stars' that has the number 5 in it. stars 5

For the CoffeeReview class, we need a stars bucket that might or might not have a value in it. When the guest leaves a rating, the bucket needs to contain that value, but when the guest forgets to rate it, that bucket needs to be empty.

So we want a bucket where we can either put a value inside of it, or leave it empty. This brings up two new terms:

  • When a variable has a value inside of it, we’ll say that the value is present.

  • When a variable does not have a value inside of it, we’ll say that the value is absent.

The 'stars' bucket... once with the number 5 in it, and once without any number in it. stars 5 stars Value is present Value is absent

Kotlin uses the keyword null to represent the absence of a value.1 A variable that is assigned null is like a bucket that’s empty.2

When a review omits the star rating, we want to set stars to null. We can try giving it a null when we construct Lucy’s CoffeeReview, but when we do that, we get an error.

val lucyReview = CoffeeReview("Lucy", "Will buy this again!", null)
Error

In fact, even apart from the CoffeeReview class, if we simply create an Int variable called stars, we can’t assign null to it.

val stars: Int = null
Error

Why is this? Well, in some programming languages, it’s possible to assign a null to any variable. That might sound like a good idea, but it can result in lots of surprises while the code is running, because we never have any guarantees that the value of a variable is present.

In order to help prevent these kinds of problems, Kotlin won’t let us assign null to just any variable. Instead, we have to clearly indicate when a variable can be empty. How can we do this?

Nullable and Non-Nullable Types

In Kotlin, we use different types to indicate whether a variable can or cannot be set to null.

All of the types that we’ve used so far—such as String, Int, and Boolean—~require_ us to assign an actual value. We can’t just assign null to them, as we discovered above. Since they don’t let us assign null, we call them non-nullable types. In contrast, types that allow us to assign null are called nullable types.

In other words:

  • When you want to guarantee that a variable’s value will be present—that is, when the value is *required—then give the variable a non-nullable type.

  • When you want to allow a variable’s value to be absent—that is, when the value is *optional—then give the variable a nullable type.

In Kotlin, nullable types end with a question mark. For example, while Int is a non-nullable type, Int? (ending with a question mark) is a nullable type.

For every non-nullable type, a corresponding nullable type exists.

Non-nullable types: String, Int, Boolean, Circle, SchnauzerBreed; Nullable types: String?, Int?, Boolean?, Circle?, SchnauzerBreed? NON-NULLABLE NULLABLE TYPES String Int Boolean Circle SchnauzerBreed String? Int? Boolean? Circle? SchnauzerBreed?

Let’s look at our code from Listing 6.6 again.

val stars: Int = null
Error

To allow this variable to accept null, we simply change stars from the non-nullable type Int to the nullable type Int?, like this.

val stars: Int? = null

Now, stars can be set to null. Of course, it can also still be set to a normal integer value.

val saraStars: Int? = 5
val tobyStars: Int? = 4
val lucyStars: Int? = null

Compile Time and Runtime

The type of a variable tells us whether that variable can hold a null, but it cannot tell us whether it actually does hold a null. This brings up an important distinction to consider. There are things we can know when Kotlin is reading the code, and there are things we can know when we’re running the code.

  • The point at which Kotlin is reading our code is called compile time. If we’re using an IDE like IntelliJ or Android Studio, this happens while we are writing the code.

  • The point at which our computer runs our Kotlin code is called runtime.

The plumber needs to assemble the water pipes before we can turn on the faucet. That’s a lot like compile time—Kotlin is busy assembling our code into something that can be used. Once the pipes are fully assembled, we can turn on the faucet to get a drink of water. That’s what runtime is—Kotlin has finished assembling the code, and now we’re finally using it.

A plumber working on pipes - Compile Time. Someone filling a glass with water from the same pipes after they've been assembled - Runtime. Compile Time Runtime

Kotlin knows the type of a variable at compile time, which is why it knows whether it can hold a null while we’re writing the code. On the other hand, Kotlin won’t know whether a variable actually holds a null until runtime.

In some simple cases like Listing 6.9, where we’re setting the variable with a literal directly in the code, it seems obvious to us whether the value is present or absent. In fact, your IDE might even warn you when you use a non-null value with a variable that’s declared to be nullable—like saraStars and tobyStars above.

But it’s not always so obvious. Values can also come from external sources, like databases, files on a hard drive, or a keyboard that a user is typing on—and those values could be null. Similarly, when calling a function that has a nullable parameter, it’s possible that the argument is null at one call site, but present at another.

In order to check whether a variable actually has a value at runtime, we have to try converting it from a nullable type to a non-nullable type. We’ll see some cool tricks for this in a moment. But first, it’s important to understand the relationship between nullable and non-nullable types.

Even though Int and Int? are related, they’re still two different types, and we can’t just use an Int? anywhere that we would use an Int. For example, a function that expects an Int won’t work if we try to send it an Int? instead.

fun printReview(name: String, stars: Int) =
    println("$name gave it $stars stars!")

val saraStars: Int? = 5

printReview("Sara", saraStars)
Error

Why is this so? To understand this, let’s turn our attention away from the review screen, and toward the front counter, where James is taking the coffee orders!

Expecting a Nullable Type

At the moment, James is running a non-profit coffee charity, which provides warm beverages to anyone, even if they can’t pay for it. If the guest would like to donate a payment, they can, but it’s not required.

Here’s a function that models this arrangement. The payment at a charity is optional, so we’ll make the payment parameter nullable.

fun orderCoffee(payment: Payment?): Coffee {
    return Coffee()
}

Naturally, if someone orders a coffee and provides payment, James will gladly hand over a coffee.

The payment isn’t required, but he appreciates the support!

The guest donates a payment and receives a coffee. Thank you for your support! Donating this payment.

Passing a Payment argument to the orderCoffee() function, as shown in Listing 6.12 below, is like this scenario—the guest is definitely providing payment.

val payment: Payment = Payment()
val coffee = orderCoffee(payment)

Now, imagine that someone walks in and says, “This box might have a payment… or it might be empty. You can have whatever is inside.”

James says, “Even if it’s empty, that’s fine. We’re a charity, after all. Here’s your coffee!”

The guest donates a mystery box and receives a coffee. This box might be empty. Okay, here's your coffee.

When we pass a Payment? argument to the orderCoffee() function, it’s like the guest is handing James a mystery box that contains either a payment or nothing at all.

val payment: Payment? = Payment() // or you could set this to null
val coffee = orderCoffee(payment)

To summarize, a function that has a nullable parameter like Payment? is like a charity.

  • It can accept an argument that has a non-nullable type, like Payment.

  • And it can also accept an argument that has a nullable type, like Payment?.

Summary - orderCoffee can receive either 'Payment' or 'Payment?' types. val payment: Payment = Payment () orderCoffee (payment) fun orderCoffee (payment: Payment?): Coffee { return Coffee () } val payment: Payment? = Payment () orderCoffee (payment)

Expecting a Non-Nullable Type

After a while, James realized that he couldn’t get enough donations to sustain the charity, so now he’s running his coffee stand as a business. All those coffee beans cost money, and since the business doesn’t run from donations, he must receive payment in order to provide coffee to the customer.

Here’s a new version of orderCoffee() that works like a business rather than a charity. Notice that the parameter has a non-nullable type, because payment is now required.

fun orderCoffee(payment: Payment): Coffee {
    return Coffee()
}

As before, when someone orders a coffee and provides payment, James will gladly hand over a coffee.

The guest provides payment and receives a coffee. Here's your coffee. Here's my payment.

Passing a Payment variable to this function is much like this scenario—the guest is definitely providing payment, so everything works just fine, as demonstrated in Listing 6.15 below.

val payment: Payment = Payment()
val coffee = orderCoffee(payment)

Now imagine that someone orders a coffee, but instead of giving payment, holds out a box, and says, “This box might have a payment… or it might be empty. I’ll trade you whatever is inside this box for a coffee.”

“No deal!” James says. “You have to actually pay for your coffee! I can’t trade the coffee for a chance to receive payment. I have to actually receive payment!”

The guest offers a mystery box, but does not receive a coffee. This box might be empty. No deal!

Passing a Payment? variable to this function is like this scenario—the guest is either handing James a payment or nothing at all. Just like James, Kotlin says, “No deal!” (…well, actually it says, “Type mismatch.”)

val payment: Payment? = Payment() // or you could set this to null
val coffee = orderCoffee(payment)
Error

When James requires payment, he can’t accept a payment that might not be there. It must be there. So a function that has a parameter of type Payment is like a business—it cannot accept an argument of type Payment?.

Summary - this version of orderCoffee can receive a 'Payment' but not a 'Payment?'. val payment: Payment = Payment () orderCoffee (payment) fun orderCoffee (payment: Payment): Coffee { return Coffee () } val payment: Payment? = Payment () orderCoffee (payment)

To summarize, we can use a non-nullable type (e.g., Payment) where a nullable type (e.g., Payment?) is expected, but not the other way around.

Now, it’s quite possible that the customer’s box actually has a payment inside! If only they would take the payment out of the box, then they could exchange that payment for coffee! Similarly, Kotlin gives us a few different ways to safely convert a nullable type to a non-nullable type. Let’s take a look!

Using Conditionals to Check for null

Here again is the function for the coffee shop business.

fun orderCoffee(payment: Payment): Coffee {
    return Coffee()
}

When the customer tried to pay with a box that might be empty, it looked like this.

val payment: Payment? = Payment()
val coffee = orderCoffee(payment)
Error

One simple way to order a coffee in this case is to check whether payment actually has a value at runtime. In other words, we can look inside the box, and if the payment is not null, then we can order the coffee.

val payment: Payment? = Payment()

if (payment != null) {
    val coffee = orderCoffee(payment)
} else {
    println("I can't order coffee today")
}

There’s no error when we write this code, and orderCoffee() will be called when we run it. How does this work? Why can we call orderCoffee(payment) in Listing 6.19 but not Listing 6.18?

Even though we declared payment to be of type Payment? (which is nullable), inside the if block, its type changes to Payment (which is non-nullable)! Inside that block, Kotlin knows that payment must have a value, because we checked for it! This is called a smart cast.

Same code as Listing 6.19, but annotated to show the type of 'payment' on each line. Its type is 'Payment?' everywhere except inside the 'if' block (4th line), where its type has been smart cast to 'Payment'. val payment: Payment? = Payment () if (payment != null ) { val coffee = orderCoffee (payment) } else { println ( "I can't order coffee today" ) } Type of payment is Payment? here (nullable) Type of payment is Payment here (non-nullable) Type of payment is back to Payment? again here

Smart casts also work with a when conditional, like this.

when (payment) {
    null -> println("I can't order coffee today")
    else -> orderCoffee(payment)
}

So, using a conditional in this way is like opening the box, and if there’s something inside it, we order the coffee.

The guest takes payment out of the mystery box, and hands it to James, who gives him a coffee in return. Here's your coffee. Here's my payment.

Otherwise, if there’s nothing inside the box, we don’t order the coffee.

The guest opened the box, but it was empty, so he says, 'No coffee for me today...' No coffee for me today...

Using a conditional to do a smart cast is just one way to convert something that’s nullable to something that’s non-nullable! Next, let’s look at the elvis operator.

Using the Elvis Operator to Provide a Default Value

In the code above, we only ordered coffee when a value was present in the payment variable. It sure would be nice if we could order a coffee even when we don’t have payment. For example, if our payment variable is null, maybe our friend can pay for us!

val payment: Payment? = null

if (payment != null) {
    val coffee = orderCoffee(payment)
} else {
    val coffee = orderCoffee(getPaymentFromFriend())
}

This allows us to order coffee in either case. If payment actually has a value, we can use that. Otherwise, we call the getPaymentFromFriend() function, which returns a Payment value that we can use instead.

As we learned back in Chapter 3, instead of an if statement we can use an if expression, which pulls the coffee variable outside of the if and else blocks. Let’s make that small change to our code, in order to make it more concise.

val payment: Payment? = null

val coffee = if (payment != null) {
    orderCoffee(payment)
} else {
    orderCoffee(getPaymentFromFriend())
}

We’re also calling orderCoffee() in both branches, so let’s pull that out of the if and else blocks, as well.

val payment: Payment? = null

val coffee = 
    orderCoffee(if (payment != null) payment else getPaymentFromFriend())

The code highlighted in Listing 6.23 is pretty common when dealing with nullable types: check whether a value is present… if so use that value, otherwise use some default value. To make this common expression easier, Kotlin gives us the elvis operator, which is a question mark and a colon. Here’s how we can use the elvis operator to make our code more concise.

val payment: Payment? = null

val coffee = 
    orderCoffee(payment ?: getPaymentFromFriend())

This code works the same as the code in Listings 6.21, 6.22, and 6.23—it’s just shorter and easier to read. Using an elvis operator is like opening the box, and if there’s something inside it, we use that.

The guest takes the payment out of the mystery box and gives it to James in exchange for a coffee. Here's your coffee. Here's my payment.

Otherwise, if the box is empty, we get a value from somewhere else and use that instead.

The guest has an empty box, so he reaches over to friend, who provides him with payment. He gives it to James in exchange for a cup of coffee. Thanks!

Using the Not-Null Assertion Operator to Insist that a Value is Present

This one is dangerous, but in some rare cases, it can be a helpful option.

When we know for sure that a nullable variable will definitely have a value when the code is running, then we can use the not-null assertion operator, which is two exclamation marks, to evaluate it to a non-nullable type.

Here’s how it would look when ordering coffee.

val payment: Payment? = Payment()
val coffee = orderCoffee(payment!!)

The type of the payment variable is Payment?, which is nullable, but the type of the expression payment!! is Payment, which is non-nullable.

By putting !! after the variable name payment, it’s like you’re saying to Kotlin, “Trust me… when the code runs, payment will not be null!” If you’re wrong about that—if the variable is indeed null, you’ll get an error when the code runs.

val payment: Payment? = null
val coffee = orderCoffee(payment!!) // Error: KotlinNullPointerException

The not-null assertion operator is like reaching into the box, and if there’s something inside, we use that…

The guest takes the payment out of the mystery box and gives it to James in exchange for a coffee. Here's your coffee. Here's my payment.

Otherwise, if the box is empty…

Explosion - Ka-boom!

This is why the not-null assertion operator is dangerous! In the other cases above—using a conditional to check for null, and using an elvis operator—it wasn’t possible for us to get an error, because Kotlin’s rules about nullable types wouldn’t allow it. But here, we’re foregoing that null safety and taking on risk that the variable might actually be null when the code is running.

Compile-Time and Runtime Errors

We can get errors during either compile time or runtime.

  • Listing 6.18 shows a compile-time error—the IDE highlights the problem while we’re writing code.

  • Listing 6.26 above shows code that will cause a runtime error, but unlike the compile-time error, there’s no highlight to let us know that an error will happen. We only get the error while our code is running.

As a general rule, an error during compile time is more helpful than an error during runtime, because we know about it sooner. In fact, Kotlin won’t even let us run our code until we’ve fixed it! Runtime errors, on the other hand, are nefarious and they’re often more difficult to hunt down.

When we use the not-null assertion operator !!, we’re avoiding a compile-time error, but taking a risk that we could end up with a runtime error.

If you’re certain that the variable will not be null at runtime, then consider using a non-nullable type instead. If, for some reason, you can’t do that, the not-null assertion operator might be what you need. But use it only as a last resort!

The flow chart below gives some advice about when to consider using non-nullable types, smart casts, not-null assertion operators, and so on. It also mentions scope functions, which we’ll learn about in Chapter 11.

When should I use the not-null assertion operator?

A flow chart illustrating when to use the not-null assertion operator. The previous paragraph describes this flow. Are you sure this variable will never be null here at runtime? Can you use a non-nullable type instead? Can you use a smart cast, elvis, or scope function? Are you sure about that? Use a non-nullable type instead Consider the not-null assertion operator Use a smart cast, elvis, or scope function Yes Yes Yes Yes No No No No

There’s one more null-safety tool that Kotlin gives us. Let’s check it out!

Using the Safe-Call Operator to Invoke Functions and Properties

Back in Chapter 4, we saw how objects have functions and properties, and we can call those by using a dot operator. For example, let’s say our Payment class has a property that tells us what type of payment the customer is using, whether cash, a check, or a card.

enum class PaymentType {
    CASH, CHECK, CARD;
}

class Payment(
    val type: PaymentType = PaymentType.CASH
)

In this case, when we get a Payment, we probably want to do something with it, like print out the type. Let’s update the orderCoffee() function to do that.

fun orderCoffee(payment: Payment): Coffee {
    val paymentType = payment.type.name.lowercase()
    println("Thank you for supporting us with your $paymentType")
    return Coffee()
}

This works great when the payment parameter is a non-nullable Payment type. But when its type is a nullable Payment? type—as it was when James was running the charity—then we get a compile-time error.

fun orderCoffee(payment: Payment?): Coffee {
    val paymentType = payment.type.name.lowercase()
    println("Thank you for supporting us with your $paymentType")
    return Coffee()
}
Error

Why is that?

Remember, a variable that has a nullable type—such as Payment?—is like a bucket that might be empty or might have a value… and we won’t know whether it’s empty until runtime. If the bucket is indeed empty while the code is running, then there would be no actual payment to get the type from.

In other words, if the payment isn’t there, then neither is a payment type! It’s not safe to get the type unless we know that the value of payment is present. Thankfully, Kotlin gives us a compile-time error, forcing us to deal with this fact.

In this chapter, we’ve already learned a few tricks that could help us. For example, we could use an if to do a smart cast.

fun orderCoffee(payment: Payment?): Coffee {
    val supportType = if (payment == null) {
        "encouragement"
    } else {
        payment.type.name.lowercase()
    }

    println("Thank you for supporting us with your $supportType")
    return Coffee()
}

When this code runs, if payment is null, we’ll print “Thank you for supporting us with your encouragement”. Otherwise, the message will be based on the type of payment, such as “Thank you for supporting us with your cash”.

This certainly works, but it’s a lot of code to write. Kotlin provides us with a safe-call operator ?. that can do the same thing, just more concisely. We can use it along with the elvis operator to achieve the same thing as Listing 6.30, like this.

fun orderCoffee(payment: Payment?): Coffee {
    val supportType = payment?.type?.name?.lowercase() ?: "encouragement"
    println("Thank you for supporting us with your $supportType")
    return Coffee()
}

So, how does the safe-call operator work?

When we look at payment.type.name.lowercase(), it kind of looks like a train.3

A train with cars that have labels for payment, type, name, and toLowerCase(), each separated by a dot. lowercase() name type payment . . .

The main change in Listing 6.31 is that we replaced the train car connectors—where we previously used a dot, we now use a safe-call operator.

The same train, but with each separated by a safe-call operator, '?.' payment lowercase() name type

When Kotlin evaluates a “train” expression like this, you can imagine that it’s hopping from car to car, left to right. When the next connector is a safe-call operator, it asks, “Does this car’s expression evaluate to null?” If so, then it hops off the train with a null.

The Kotlin logo bouncing off of the first car with a null. null lowercase() name type payment

Otherwise, it hops to the next car, repeating the process until it finds a null or jumps off the caboose with the final value.

The Kotlin logo bouncing off of each car in succession, then hopping off of the caboose with the payment type string 'cash'. cash lowercase() name type payment

Generally, when writing a train expression, if one of the cars has a nullable type, the rest of the train connectors after it will need to be safe-call operators rather than just dot operators. We’ll see an exception to this when we get to extension functions in Chapter 10.

Summary

Enjoying this book?
Order the paperback today!

Kotlin: An Illustrated Guide is now available on Amazon See the book on Amazon

James’ coffee stand has come a long way! He learned that a zero-star rating isn’t the same as having no star rating. He started off as a charity that could accept payment, and then brewed into a full-fledged business that must receive payment. And Kotlin’s null-safety features helped him avoid some bitter surprises along the way! Here’s what we covered in this chapter.

Proper handling of nulls is an essential skill for every great Kotlin programmer. In the next chapter, we’ll learn about another essential concept—lambdas!

Thanks to Louis CAD and James Lorenzen for reviewing this chapter.


  1. In some Latin-based languages, the word “null” is more closely related to the number zero, but in English it more often refers to something that has no value or effect. When you see it in Kotlin, don’t think of it as the number zero; think of it as “not having a value”. ↩︎

  2. Technically, even null can be considered a value. So, you might hear someone say, “The value of that variable is null,” and that’s fine. However, since the concepts of “present” and “absent” are more intuitive, in this chapter, we’ll regard null as the absence of a value rather than as a value itself. ↩︎

  3. In fact, the term “train wreck” has been used to describe expressions like this that have many function or property calls chained together. In this book, I won’t add commentary about the advantages or disadvantages of expressions like these. So, instead of calling it a train wreck, I’ll just call it a train! ↩︎

Share this article:

  • Share on Facebook
  • Share on Reddit