Kotlin Uncovered: Part 4
Learning about Kotlin nullability through decompilation
Apple Laptop Work by Unsplash is licensed under CC0
I’ve found decompiling Kotlin bytecode into Java to be a great tool to help understand what’s going on. If this is new to you, check out Part 1. I’ll wait here for you to get back.
One of the things we hear about a bunch when it comes to Kotlin is that nullability is built right into the type system. This means that we always know when something could be null, and the compiler forces us to check for it. By doing this, we run a much lower risk of encountering a NullPointerException
.
We notate that a variable could be null by putting a question mark after the type, e.g. String?
. If there is a question mark, it’s nullable. Otherwise, you’re safe. If a variable is nullable, and we don’t check for null before we call something on it, it won’t compile. Such is the case below. Since maybeString
is nullable and we don’t check for null before we call length on it, it won’t compile.
// Won't compile!! var maybeString: String? = "Hello" maybeString.length
Safe call operator
Because that one won’t compile, we’ll use a different example to decompile. This one uses the safe call operator. That’s the question mark after maybeString
, and before .length
. What this does is call length
if maybeString
is not null and return null otherwise.
// Kotlin val maybeString: String? = "Hello" maybeString?.length
When I decompiled this into Java, it confused me at first. I didn’t see any null safety!
// Java String maybeString = "Hello"; maybeString.length();
This is where I remembered of all the work the compiler does for us. Let’s look at an example where we don’t know if maybeString
is null
. We’ll use a method with the signature fun getString(): String?
to set this value.
// Kotlin val maybeString: String? = getString() maybeString?.length
// Java String maybeString = this.getString(); if(maybeString != null) { maybeString.length(); }
Okay, the null check is there this time. Because we were assigning a non-null value to an immutable variable in the first example, the compiler could conclude that there was no way for maybeString
to be null. As a result, it removed the extra code for us. If the compiler is doing this, imagine all the other ways it’s helping us!
!! Operator
If you’re looking for a NullPointerException
, Kotlin does have an option for that. By using the !! operator, we’re telling the compiler that we are confident that it’s not null, so we can use the value of a variable without checking for null first.
// Kotlin val maybeString: String? = getString() maybeString!!.length
When we decompile it into Java, we can see that there is an explicit check for null, and it throws the NPE if needed before our code even gets to call anything on it.
// Java String maybeString = this.getString(); if(maybeString == null) { Intrinsics.throwNpe(); } maybeString.length();
Null Safe Scoping
By combining the null safe operator with a higher order function, such as let
, we can get null safe scoping. We can pass a lambda to let
, and it is only executed if the value is not null. Otherwise, it does nothing. Inside the block, the value is assigned to a variable, it
.
// Kotlin val maybeString: String? = getString() return maybeString?.let { // it == maybeString it.length }
When we decompile it, we can see that it uses a ternary operator inline to check if maybeString
is null
before returning the length or null
. It’s a bit underwhelming with this small code sample, but it can be useful when you have multiple lines of code that depend on a value not being null
. With more lines, it would also use an if/else
block.
// Java String maybeString = this.getString(); return maybeString != null ? Integer.valueOf(maybeString.length()) : null;
Elvis Operator
One last null safety option we’ll look at here is the Elvis operator. By combining it with the safe call operator, we can easily give a default value if maybeString
is null. Here, we are saying that if maybeString
is null, return zero.
// Kotlin val maybeString: String? = getString() return maybeString?.length ?: 0
Then, when we decompile it, we get the ternary we might expect. If maybeString
is not null, return the length, otherwise, return zero.
//Java String maybeString = this.getString(); return maybeString != null ? maybeString.length() : 0;
Kotlin provides greater null safety than Java and gives us some clean tools for us to work with nullability. Lesser risk for NPEs is one of the many reasons I enjoy working with Kotlin.
Need to go back and refresh your memory on other parts of the series? You can find them below:
Comments
It’s not the compiler who selects if/else or ternary, it is the decompiler choosing how to represent the same conditional bytecode constructions.