Null-Safe Programming: The Kotlin Way
This overview of null-safe programming considers Java's default approach, what Optionals brought to the table in Java 8, and how Kotlin handles null safety.
Join the DZone community and get the full member experience.
Join For FreeAs Java developers, we're very accustomed to NullPointerExceptions (NPEs) 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 aren'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<Device> 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<Device>, 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).
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?
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 NullPointerExceptions.
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 vars 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, for example, 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. NullPointerExceptions 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.
Opinions expressed by DZone contributors are their own.
Comments