Kotlin null safety and its performance considerations -- part 1

Kotlin null safety and its performance considerations -- part 1

Konrad Kamiński

Kotlin may seem like a new kid on the block – itʼs been officially released only in February. Its history however dates a few years back and itʼs mature and stable enough to be used for developing solid reliable applications. Therefore at Allegro we decided to give it a chance – we built our new shiny server-side system using Kotlin as its primary language and we do not regret it.

One of the first features a Kotlin developer learns is the languageʼs approach to handling null values. It is quite interesting – especially at times like these when the most popular way of handling this problem is to use some kind of Option monad. As weʼll soon see Kotlin actually does not introduce any new special wrapper type – it uses regular Java types albeit with slight variance.

Null-safe world

In Kotlin when you declare a variable, a field or a function parameter, by default they cannot be null. For example letʼs suppose we have a class Greeter which has a function hello that by default prints a greeting message on the standard output:

We declared who to be of type String which is interpreted by the compiler to mean that who cannot be null. If we want to declare a nullable parameter we have to add a question mark at the end of the type name:

This simple solution turns out to be very convenient and strong at the same time. It divides the world of our code into two areas: one where nulls are allowed and one where they arenʼt. As weʼll see in a moment Kotlin provides quite a range of helpful features which makes the transition between those areas entirely safe. Yet we have to be aware of a few surprising issues.

To fully explore how Kotlin handles nulls underneath weʼll take a closer look at the code generated by Kotlin compiler. Weʼll do it on two levels: first weʼll inspect the output bytecode – although for brevity weʼll actually see equivalent Java code. Then, in the second part of this article – for some interesting cases weʼll have a glance at the machine code generated by JVM JIT compiler.

Smart casts

Since Kotlin compiler knows the type of every variable, field, function parameter, etc. it can check if an incorrect assignment takes place and throw an error in such case. In the code below we try to assign a nullable reference to a non-null property:

This construct – where the compiler can infer that the reference cannot be null – is called smart casting as it seems to cast the reference from String? to String.

There is one thing we have to bear in mind when using smart casts. We can only do it with references that cannot be changed between the checkpoint and the actual assignment. In the code above the reference was taken from the function parameter which cannot change in the course of function execution. If we were to take the reference from some read/write property the compiler would not allow it:

The !! operator

The code shown in the above example looks quite common. When we expect a reference to be not null and this assumption proves wrong we may want to throw NullPointerException. Kotlin has a special syntax for such cases:

The newName parameter of the setName method gets an annotation indicating its nullability. This annotation is used internally by Kotlin compiler and IntelliJ IDEA, although one can think of using it also in tools like FindBugs or Checker Framework.

The code in setName has a striking resemblance (surprise!) to the code we wrote in our smart cast example. The only difference is the usage of the throwNpe method. Weʼll delve into the details in part 2 – for now we can make a simplification and state that it throws KotlinNullPointerException (which extends NullPointerException).

Parameter validation

So far in our examples we didnʼt have methods which were not private and at the same time had non-null parameters. In order to gain some insight into what happens in such situations letʼs modify our code:

We can see that we now have two additional methods (getUsername and setUsername) and the username field earned a @NotNull annotation. Weʼre witnesses to how Kotlin compiler manages properties in classes:

If it is private then a property is simply a field of the class with no special annotations – this is because this property is not visible anywhere outside the class and therefore Kotlin compiler can optimize access to it and it can be sure that the field will always be non-null.

If it is not private then a property is actually a field with a pair of setter and getter methods – this is because the property is visible to the outside world and Kotlin compiler must check upon every access that it is non-null and at the same time provide this information to this outside world.

We can also observe that to check whether a value of a parameter is not null the checkParameterIsNotNull method is used. Again weʼll investigate this method in part 2. For now it is enough to say that upon receiving a null value an IllegalArgumentException will be thrown.

Elvis operator

When we have a null value there are situations when instead of throwing an exception weʼd rather do something else. We can compare this to a default block in Java switch statement. A simple if statement with an else should suffice here, but Kotlin has a special syntax for it – the famous Elvis operator.

Letʼs suppose that for a null value weʼd like to set username to "N/A":

The Kotlin syntax is more concise and itʼs also worth mentioning that the expression after the Elvis operator is lazily evaluated and it can also throw an exception. So itʼs completely legal to have this kind of code:

Safe calls and let/run/apply functions

There are circumstances when we have a possibly null object and we want to invoke a method on it but only if it is actually non-null (because otherwise we would get NullPointerException). We can do it with simple if, but Kotlin provides a fancy ?. operator to make the code more compact. So this code:

Kotlin code is indeed a lot shorter and more expressive. To further aid developers Kotlin provides three convenient higher-order functions: let, run and apply. Although they are not directly related to null-safety issues we often use them with potentially null objects. Below you can find some code examples – they should give you an intuition about when and how to use them.

let

let is essentially an extension function (i.e. a method of a class which you can define outside of the class definition) that can be invoked on any type. It takes a lambda expression as its parameter and calls the expression with this as an argument. If a lambda expression has only one parameter then we may skip declaring it and simply access the parameter via the name... it. Therefore in the example above this inside the lambda (this is of type SimpleUser – the type returned by the getUser method) is accessible as it. The value of the lambda expression is returned as the result of the let function.

let is usually used if we want to perform some operations on a non-null object and return the result of these operations while simply returning null for null objects.

run

The run function is a slight variation of let. It takes a parameterless lambda expression as its parameter and the object on which you invoke run can be accessed via this inside the lambda expression. Just like in let the result of the lambda expression is returned as the result of the run function.

apply is similar to run – it is an extension function which takes a parameterless lambda expression as its parameter and the object on which you invoke apply can be accessed via this inside the lambda expression. However, the return value of apply is the object on which you invoke it (and not the lambda expression result).

The most common reason to use apply is the initialisation of an object. If there is something we have to do before returning an object (and simply return null if it is null) then apply is the way to go.

Platform types

So far we have not talked about the interoperability with Java. While from Java code point of view with regard to null-safety nothing unusual happens if we call Kotlin code, there is a difference when we want to call Java code from Kotlin. This is especially important for the case of the result returned from Java method where Kotlin compiler has to take some precautions. After all it does not know if the value returned can be null or not.

To resolve this problem Kotlin introduces the concept of platform types. In essence platform type is used every time Kotlin compiler encounters an invocation of Java method which was not generated by Kotlin compiler. At the same time a developer cannot explicitly declare anything to be of platform type – it exists solely when Kotlin compiler infers it from the code.

Letʼs look at some code samples which present most of things you have to know about platform types:

Now we have a validation in the generated code. writeOut expects a non-null type, but the value type is platform type and in theory it could be null. Therefore the compiler produces a runtime check with the help of checkExpressionValueIsNotNull method which weʼll explore in part 2.

As we can see the rule is simple – every time we go from the plaform type to non-null type a runtime check is generated. Once weʼre in the null-safe world no additional validation is needed.

In the examples above weʼve seen that the checkExpressionValueIsNotNull method takes a variable name as the second parameter. This is done so that when a null value is passed youʼll see an error message with the name of the variable in it. But as the name of the method implies the second parameter does not have to be a variable name. In fact itʼs always an expression name, but in the cases presented above we had simple one-variable expressions. If we had a more complicated expression...:

If we now call funnyCase (and there is no "key" system property set) then weʼll see the following stack trace:

Exception in thread "main" java.lang.IllegalStateException: System.getProperty("key") must not be null
at pl.kk.test.kotlin.PlatformTypesKt.funnyCase(PlatformTypes.kt:54)
at pl.kk.test.kotlin.FunnyCaseCall.main(PlatformTypes.kt:60)

Summary

Weʼve taken a tour of different Kotlin language constructs where you could observe the code generated by the compiler. During regular development you rarely have to think about how things work under the hood. Nonetheless it is useful to know a thing or two about it. And if youʼre interested in the performance issues surrounding some of those constructs check out the second part of this article which will be soon published.