Konrad Kamiński

Kotlin null safety and its performance considerations -- part 1

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:

class Greeter {
  fun hello(who: String): Unit {
    println ("Hello $who")
  }
}

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:

class Greeter {
  fun hello(who: String?): Unit {
    println ("Hello $who")
  }
}

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:

class User {
  private var username: String = ""

  fun setName(newName: String?) {
    username = newName // the compiler will indicate an error here
  }
}

What if we wanted to check if the reference is not null and only then assign it to a property? The following code provides an answer:

class User {
  private var username: String = ""

  fun setName(newName: String?) {
    if (newName == null)
      throw NullPointerException("Name cannot be null!")

    username = newName // here the compiler knows that newName cannot be null and therefore
  }                    // its type is String and not String?
}

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:

class UserRequest {
  var username: String? = null
}

class User {
  private var username: String = ""

  fun setNameFrom(request: UserRequest) {
    if (request.username == null)
      throw NullPointerException("Name cannot be null!")

    username = request.username // here the compiler cannot be sure that request.username
  }                             // is not null - it could change in some other thread
}                               // right after the condition check

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:

class User {
  private var username: String = ""

  fun setName(newName: String?) {
    username = newName!! // the type of newName!! is String and the compiler generates a runtime check to be sure of that...
  }
}

The equivalent Java code as taken from the bytecode generated by the Kotlin compiler is as follows:

import org.jetbrains.annotations.Nullable;

import static kotlin.jvm.internal.Intrinsics.throwNpe;

public final class User {
  private String username = "";

  public void setName(@Nullable newName: String) {
    if (newName == null) {
      throwNpe();
    }

    username = newName;
  }
}

We can see a couple of interesting things here:

  1. 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.
  2. 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:

class User {
  var username: String = ""

  fun setName(newName: String) {
    username = newName
  }
}

One obvious method that weʼd like to have a look at is the setName method. But the equivalent Java code contains a surprise:

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull;

public class User {
  @NotNull
  private String username = "";

  @NotNull
  public String getUsername() {
    return username;
  }

  public void setUsername(@NotNull String value) {
    checkParameterIsNotNull(value, "<set-?>");

    username = value;
  }

  public void setName(@NotNull String newName) {
    checkParameterIsNotNull(value, "newName");

    username = value;
  }
}

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:

  1. 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.
  2. 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":

class User {
  private var username: String = ""

  fun setName(newName: String?) {
    username = newName ?: "N/A"
  }
}

The equivalent Java code is:

import org.jetbrains.annotations.Nullable;

public class User {
  private String username = "";

  public void setName(@Nullable String newName) {
    username = (newName != null) ? newName : "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:

class User {
  private var username: String = ""

  fun setName(newName: String?) {
    username = newName ?: throw RuntimeException("Are you nuts?")
  }
}

The equivalent Java code is:

import org.jetbrains.annotations.Nullable;

public class User {
  private String username = "";

  public void setName(@Nullable String newName) {
    if (newName != null)
      username = newName;
    else
      throw new RuntimeException("Are you nuts?");
  }
}

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:

data class SimpleUser(var name: String)

object Users {
  private val userMap = mapOf(1 to SimpleUser("John"))

  @JvmStatic
  fun getUser(userId: Int): SimpleUser? = userMap[userId]
}

fun getUserName(userId: Int): String? =
        getUser (userId)?.name

has the following equivalent Java code for getUserName:

@Nullable
public String getUserName(int userId) {
  SimpleUser simpleUser = getUser(userId);
  return simpleUser != null ? simpleUser.getName() : null;
}

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ʼs look at the following code with let (pardon the pun):

fun findUserNameWithLet(userId: Int): String? =
    getUser (userId)?.let { it.name }

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.

The equivalent Java code for findUserNameWithLet is as follows:

@Nullable
public String findUserNameWithLet(int userId) {
  String result;

  SimpleUser simpleUser = getUser(userId);
  if (simpleUser != null) {
    SimpleUser it = simpleUser;

    result = it.getName(); // { it.name }
  } else {
    result = null;
  }

  return result;
}

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.

Letʼs see an example:

fun findUserNameWithRun(userId: Int): String? =
    getUser (userId)?.run { name }

and the equivalent Java code:

@Nullable
public String findUserNameWithRun(int userId) {
  String result;

  SimpleUser simpleUser = getUser(userId);
  if (simpleUser != null) {
    SimpleUser $receiver = simpleUser;

    result = $receiver.getName(); // { name }
  } else {
    result = null;
  }

  return result;
}

As we can see there is little difference with let — Kotlin code is similar although with run it is even more concise.

apply #

The last of the three convenient functions weʼre going to talk about is apply. The following code illustrates its usage:

fun findUserNameWithApply(userId: Int): SimpleUser? =
      getUser (userId)?.apply { name = "Jane" }
}

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 equivalent Java code for findUserNameWithApply is as follows:

@Nullable
public SimpleUser findUserNameWithApply(int userId) {
  SimpleUser result;

  SimpleUser simpleUser = getUser(userId);
  if (simpleUser != null) {
    SimpleUser $receiver = simpleUser;

    $receiver.setName("Jane"); // { name = "Jane" }

    result = simpleUser;
  } else {
    result = null;
  }

  return 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:

fun writeOutNullable(s: String?) = println(s)

fun nullableCase() {
  val value = System.getProperty("key") // value has a platform type String!
  writeOutNullable(value)               // The ! after a type indicates it's a platform type
}

The equivalent Java code is:

public void nullableCase() {
  String value = System.getProperty("key");
  writeOutNullable(value);
}

We can see nothing special here — the platform type is taken as is when it is converted to a nullable type.

Letʼs see what happens when the target type is non-null:

fun writeOut(s: String) {
  println(s)
}

fun nonNullCase() {
  val value = System.getProperty("key") // value has a platform type String!

  writeOut(value)
}

The equivalent Java code is:

import static kotlin.jvm.internal.Intrinsics.checkExpressionValueIsNotNull;

public void nonNullCase() {
  String value = System.getProperty("key");

  checkExpressionValueIsNotNull(value, "value");
  writeOut(value);
}

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.

Next example shows the code where only Java methods are used.

fun pureJavaCase() {
  val value = System.getProperty("key") // value has a platform type String!

  System.setProperty("other_key", value)
}

The equivalent Java code is:

public void pureJavaCase() {
  String value = System.getProperty("key");

  System.setProperty("other_key", value);
}

Just like when we had the nullable types no runtime check is generated. After all at no place in the code do we pass the value to the code which explicitly expects non-null values.

One might ask what happens after the value is checked not to be null — does the compiler treat it as a non-null value?

fun doubleCheckCase() {
  val value = System.getProperty("key") // value has a platform type String!

  writeOut(value)
  writeOut(value)
}

The equivalent Java code is:

public void doubleCheckCase() {
  String value = System.getProperty("key");

  checkExpressionValueIsNotNull(value, "value");
  writeOut(value);
  checkExpressionValueIsNotNull(value, "value");
  writeOut(value);
}

It appears it doesnʼt — the runtime checks are generated each time the conversion to non-null type takes place.

The following example shows assigning the value with platform type to some variable without explicitly stating this variable type:

fun assignCase() {
  val value = System.getProperty("key") // value has a platform type String!
  val newValue = value // newValue has a platform type String!

  writeOut(newValue)
}

The equivalent Java code is:

public void assignCase() {
  String value = System.getProperty("key");
  String newValue = value;

  checkExpressionValueIsNotNull(newValue, "newValue");
  writeOut(newValue);
}

No surprise here — the type of the variable newValue is also a platform type and runtime checks are generated.

What if we explicitly state the variableʼs type?

fun explicitCase() {
  val value = System.getProperty("key") // value has a platform type String!

  val nullableValue: String? = value
  writeOutNullable(nullableValue)

  val nonNullValue: String = value
  writeOut(nonNullValue)
  writeOut(nonNullValue)
}

The equivalent Java code is:

public void explicitCase() {
  String value = System.getProperty("key");

  String nullableValue = value;
  writeOutNullable(nullableValue);

  checkExpressionValueIsNotNull(value)
  String nonNullValue = value;
  writeOut(nonNullValue);
  writeOut(nonNullValue);
}

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…:

fun funnyCase() {
  writeOut(System.getProperty("key"))
}

The equivalent Java code is:

public void funnyCase() {
  String tmp = System.getProperty("key");
  checkExpressionValueIsNotNull(tmp, "System.getProperty(\"key\")");
  writeOut(tmp);
}

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.

Discussion