Null-Safe Programming - The Kotlin Way
Disclaimer: This ktor article was originally published in the Dzone Java Guide 2018, which can be downloaded here.
In this article, we will review the problems that may be caused by null pointers and how to avoid them in Java. After that, the article demonstrates how Kotlin nullability features work and how they improve your code.
As Java developers, we're very accustomed to NullPointerException
s (NPE) that are thrown at the runtime of an application. This almost always happens unintentionally in consequence of a bug, which is based on unrecognized references to null
. The null
reference is often used to indicate absent values, which isn't obvious to the programmer in many cases. Although Java relies on strong static typing, it doesn't let you distinguish between reference types that can and cannot hold a null
reference. Have a look at the following code example:
Device audio = new DeviceProvider().getAudioDevice();
String audioName = audio.getName();
The method getAudioDevice
returns an object of type Device
but might return null
in order to denote the absence of that device on particular systems. Well documented methods will describe exactly that behavior, which still requires the developer to be very attentive. Not knowing about the possibility of a returned null
reference is going to cause an awful NullPointerException
in the subsequent call to getName
. Wouldn't it actually be nice if we were able to identify the method's returned Device
type as nullable (or non-nullable respectively) firsthand?
Null-Safety in Java
We have to find strategies that help us avoiding unwanted bugs due to NPEs. A common approach is to defensively check for null
references and handle these cases in a way that makes more sense, such as providing default values or throwing more meaningful exceptions. Applying this to the previous example brings us to the following solution:
Device audio = new DeviceProvider().getAudioDevice();
String audioName;
if (audio != null) {
audioName = audio.getName();
} else {
throw new IllegalStateException("This system does not provide an audio device.");
}
Constructs like these are part of any Java project and the approach works well unless you forget to add checks for certain variables. It's a fairly error-prone approach and doesn't mitigate the fact that using null
as a representative for absent things is risky.
Does Java offer any more sophisticated solutions to this problem? It does, at least in some situations. The Java SE 8 introduced the Optional
type that acts as a "container object which may or may not contain a non-null value". This sounds very promising and needs to be considered next.
Java SE 8 Optional
The Optional
type is a container that wraps another object, which can theoretically be null
. Code that works on instances of Optional
needs to handle the possible nullability in a rather explicit way:
Optional audio = new DeviceProvider().getAudioDevice();
String audioName = audio
.flatMap(Device::getName)
.orElseThrow(() -> new IllegalStateException("This system does not provide an audio device."));
The getAudioDevice
method was adjusted to return an Optional
, which doubtlessly indicates to the client that the device can be absent. Instead of carelessly accessing that nullable type, the Optional
type can be used for handling the possible nullability in various ways. These include providing default values and throwing exceptions with the help of simple methods like orElse
and orElseThrow
respectively. Furthermore, it literally forces the client to think about the potential null
case.
Unfortunately, the whole Optional
story already ends with this use case very suddenly. As stated by Java language architect Brian Goetz in this StackOverflow post, the Optional
type was not intended as a "general purpose Maybe [...] type" but a way to let libraries and APIs express the absence of a return type (as we saw in the previous example).
For further usage examples of Optional
, you should find a suitable article in your preferred search engine.
After all, the Optional
type is a great way to provide more explicit APIs that let the corresponding callers know exactly when null
handling is required just by observing the method's signature. Nevertheless, it's not a holistic solution since it isn't meant to replace each and every null
reference in the source code. Aside from that, can you safely rely on method return types, which are not marked as Optional
?
Kotlin Nullability: Null-Safety in Kotlin
After we have seen the rather unsafe null handling in the Java language, this section will introduce an alternative approach: The Kotlin programming language, as an example, provides very sophisticated means for avoiding NullPointerException
s.
The language's type system differentiates between nullable and non-nullable types and every class can be used in both versions. By default, a reference of type String
cannot hold null
, whereas String?
allows it. This distinction on its own doesn't make a very big difference obviously. Therefore, whenever you choose to work with nullable types, the compiler forces you to handle possible issues, i.e. potential NPEs, appropriately.
//declare a variable with nullable String type, it's OK to assign `null` to it
var b: String? = "possiblyNull"
// 1. does not compile, could throw NPE
val len = b.length
// 2. Check nullability before access
if (b != null){
b.length
}
// 3. Use safe operator
val len = b?.length
This code example shows different ways of working with nullable types (String?
in this case). As demonstrated first, it's not possible to access members of nullable types directly since this would lead to the same problems as in Java. Instead, traditionally checking whether that type is not null makes a difference. This action persuades the compiler to accept invocations on the variable (inside the if
-block) as if it were not nullable. Note that this does not work with mutable var
s because they could possibly be set to null
from another thread between the check and first action in the block.
As an alternative to explicit checks, the safe call operator ?.
can be used. The expression b?.length
can be translated to "call length
on b
if b
is not null
, otherwise return null
". The return type of this expression is of type Int?
because it may result in null
. Chaining such calls is possible and very useful because it lets you safely omit a great number of explicit checks and makes the code much more readable:
person?.address?.city ?: throw IllegalStateException("No city associated to person.")
Another very useful operator is the elvis operator ?:
that perfectly complements the safe call operator for handling else
cases. If the left-hand expression is not null
, the elvis operator returns it, otherwise, the right-hand expression will be called.
The last operator that you need to know is called not-null assertion operator !!
. It converts any reference to a non-null type. This unchecked conversion may cause an NPE if that reference is null
after all. The not-null assertion operator should only be used with care:
person!!.address!!.city //NPE will be thrown if person or address is null
In addition to the shown operators, the Kotlin library provides plenty of helpful functions like String?::IsNullOrEmpty()
, String::toDoubleOrNull()
and List::filterNotNull()
, to name just a few. All of them support the developer in proper nullability handling and make NPEs almost impossible.
Interop between both languages
One of the key attributes of Kotlin is its fantastic interoperability with Java source code. You can easily mix both languages in a project and call Kotlin from Java and vice versa. How does that work in the case of null safety, though?
As learned earlier in this article, every Java reference can be null
, which makes it hard for Kotlin to apply its safety principles to them meaningfully. A type coming from Java is called platform type, denoted with an exclamation mark, e.g. String!
. For these platform types, the compiler isn't able to determine whether it's nullable or not due to missing information. As a developer, when a platform type is e.g. assigned to a variable, the correct type can be set explicitly. If you assign a platform type String!
to a variable of the non-nullable type String
, the compiler allows it and, as a consequence, safe access to that variable isn't being enforced. Nevertheless, if that decision turns out to be wrong, i.e. a null
reference is returned, NPEs will be thrown at runtime. Fortunately, there's a solution that allows providing more information to the Kotlin compiler by applying certain annotations to the corresponding Java methods. This enables the compiler to determine actual nullability information and makes platform types unneeded.
Bottom Line
It's important to understand that Kotlin does not try to avoid null
references as such but raises the attention for null
-related issues and especially NPEs enormously. If you take care of platform types and defensively decide to use them as nullable ones or apply the mentioned annotations to your Java libraries, you should be safe. NullPointerException
s will be a thing of the past. As set out above, the Kotlin language provides many useful operators and other functions that simplify working with nullable types tremendously by making use of its clever type system. We can hope to see similar solutions in future Java versions soon.
Simon is a software engineer with 9+ years of experience developing software on multiple platforms including the JVM and Serverless environments. He currently builds scalable distributed services for a decision automation SaaS platform. Simon is a self-appointed Kotlin enthusiast.
Isn’t the true problem that optional isn’t designed as a general purpose “Maybe”-type? I would argue that we should still do our best avoid nulls in all cases, and especially as return types.
For instance there’s the semantic problem, because what does a null value actually mean? Is it deliberate? Is it due to some error (e.g., badly written SQL returning a null that shouldn’t be there)? What does it mean? Even though Kotlin sees the “?” and I assume it adds the @NotNull annotation in “pure” Java, which some IDEs will use to create appropriate warnings, I do not believe this contract is strong enough.
Also seen from the logic of data/memory it’s also a strange amoeba. It either points at an either non-existing memory location or a memory location with garbage data. Both is nonsense and just creates a can of worms. Fortunately this is not C/C++ where it’s even worse. Where as the correct solution points to a memory location that explicitly say – I represent the empty object.
Unfortunately in Java (since Option is flawed, as it is not designed as a proper Maybe representation) that requires us to either use the NullObject pattern. Or at a minimum ensure that one never ever return null, by for instance using null as the internal value, but always returning Optional. I think the former is the most correct, but I personally usually fall back on the latter for pragmatism.
Null is a legacy of poor design and should IMO be avoided at all costs when possible. Even the creator of the null reference said so. It has a bit of the same feel as when a huge amount of people was advocating ASP.net over PHP, their favor could be boiled down to the superior template system for injecting server side code into the client code – basically a better way of doing things wrong. Just because there are nice tools for dealing with the mistake doesn’t mean that we should embrace it and start using it
The comparison with asp.net vs php at the end of the article doesn’t feel like an honest conclusion. There are a lot more reasons than templates for advocating any typesafe, mature language and platform vs the mess that is php.