Kotlin Isn’t Null-Safe Yet. Here Are 3 Gotchas

Hidden causes of null-pointer exceptions that the compiler won’t even try to protect you from

Sam Cooper
Better Programming

--

Photo by Xavi Cabrera on Unsplash

Null-safety is one of Kotlin’s biggest selling points, and a key reason for choosing Kotlin over Java.

In Kotlin, every type specifies whether it can include null values or not. But the null-safety guarantees that this enables might not be as bulletproof as we’d like to think. I’m not talking about things like Java interoperability, which bends the rules on purpose. Failures in Kotlin’s type system mean a non-null type can end up hiding a null value, even in pure Kotlin code.

Here are three pitfalls that will earn you a NullPointerException even when the Kotlin compiler positively guarantees you that your value isn’t null.

1. Objects that depend on each other

Before you run this code, have a read through it and make your best prediction about what it will do.

According to the compiler, Left.value and Right.value are both non-null. But a quick glance at the source code is enough to raise some suspicions. What would the property values actually be? There’s nowhere in the program that provides a real String for them to use.

In reality, and despite what the compiler might claim, both properties are always null.

The problem arises because singletons declared with the object keyword can be accessed from anywhere at any time.

Normally, when you initialize the properties of an object or class, the compiler prohibits forward references. Each value can depend on ones above it, but it can’t depend on subsequent properties that haven’t been initialized yet.

So an object can’t reference its own uninitialized fields directly. But it can reference another object. And if that second object contains a reference back to the first one, it may end up seeing the first object in an uninitialized state. Before initialization, even properties with non-null types will temporarily contain a null value. Copying the uninitialized value from one property to another can result in a value that’s permanently null even when the compiler claims it can’t be.

This problem is tracked in YouTrack under KT-13321.

2. Type checks with reified generics

In an inline function, generic type parameters can be marked as reified, allowing you to use the type parameter at runtime.

The built-in filterIsInstance function uses a reified generic parameter. It filters a list to remove items that don’t match the given type.

Unfortunately, it can be used to perform illegal type checks that don’t actually work!

In this example, we start with a list of lists, and try to use filterIsInstance to remove any lists that might contain nulls. The compiler is happy enough with this, and now believes that values contains only lists with non-null elements. Because of this, it lets us try to call functions like uppercase on the inner list items.

But run the code and you’ll see that the filtering didn’t actually work. The list with a null in it is still there, and trying to call the uppercase function on its single element results in a NullPointerException.

When you look at the implementation of the filterIsInstance function, you’ll see it uses the is operator to check if each value matches the expected type. The problem is, you can’t do that for parameterized types like List<T>. If you try writing that check directly in your code, you’ll get a compiler error.

When you’re using reified generics, the compiler conveniently forgets that this restriction exists, and allows runtime type checks for any type you might choose to provide.

The problem isn’t necessarily a limitation of reified type parameters, by the way. A reified generic type can happily differentiate between two types like List<String> and List<Int>. But the list itself doesn’t know what its element type is, so checking it against another type at runtime isn’t possible.

The YouTrack issue for this problem is KT-55680.

3. Using ‘this’ in a constructor

This abstract Greeter class is supposed to display some output every time a new instance is constructed.

In reality, it throws a NullPointerException.

This might be the most counterintuitive of the three issues, but it’s probably also the most familiar to anyone who’s worked with Java. The problem exists there too, and it has to do with the order that classes and their properties are initialized.

When you create a new object, its topmost superclass is initialized first. Once that’s done, initialization proceeds down the object’s class hierarchy, filling in the properties of each class in order.

In the Greeter example, the call to println implicitly calls toString on the current object. That function is overridden by the concrete Hello subclass, and its implementation tries to access the greeting property. But at the moment when toString is called, we’re still in the middle of initialising the Greeter superclass. The greeting property doesn’t have a value, because it hasn’t been initialized yet.

It shouldn’t be surprising that using this in a constructor can have strange behaviour. The this keyword refers to the current object, and inside the constructor it goes without saying that the current object is still in the middle of being constructed. But you don’t need to explicitly use the word this to run into the problem. Using any function or property of the current class is effectively a reference to this. If the function or property is non-final, it can end up executing code in a subclass before the subclass properties have been initialized.

Of the three issues I’ve mentioned, this is the only one that currently generates any kind of warning. If you squint, you’ll see a faint squiggly underline in IDEA when you use this in a constructor.

You can follow this issue in YouTrack under KT-10455.

Wrap-up

What do you think of these three null-safety issues?

Do they represent serious bugs in the Kotlin type system, or are they just quirks that rarely affect real code? Would they put you off from using Kotlin, or are you happy to accept them as minor pitfalls to keep an eye out for? And can they even be solved, or are they fundamental language design flaws?

Let me know if you’ve ever run into any of these problems in real code, or if you know of any other ways to trick the Kotlin compiler into assigning null to a non-null type!

--

--