avatarRoman Elizarov

Summary

The article discusses the evolution of handling null references in Java, advocating for the safer and more efficient approach to nulls provided by modern programming languages like Kotlin.

Abstract

The author reflects on the long-standing struggle with NullPointerException (NPE) in Java, emphasizing the industry's recognition of null references as a "Billion Dollar Mistake." The article contrasts the traditional Java API, such as File.list(), which returns null and can lead to NPEs, with modern alternatives like Files.newDirectoryStream that avoid nulls altogether. It criticizes the verbose nature of Java's null checks and the extreme practices that have emerged to avoid nulls, such as the use of Optional wrappers and custom "null object" patterns. The author argues that the flaw lies in Java's type system, which treats null as a valid member of every type, leading to unsafe operations and runtime failures. Kotlin is presented as a solution, with a type system that distinguishes between nullable and non-nullable types, thus enabling safer API design and more readable, boilerplate-free code. The article concludes by encouraging embracing null in API design with Kotlin, illustrating the ease of handling potentially missing values with examples like parsing an integer from command-line arguments.

Opinions

  • The author believes that null references have been a significant issue in Java programming, leading to the infamous NullPointerException.
  • The article suggests that the design of Java's type system, which allows null to be a member of any type, is fundamentally flawed.
  • It is the author's opinion that modern programming languages like Kotlin have addressed this flaw by incorporating null into the type system more effectively.
  • The author views the use of Optional and "null object" patterns in Java as inelegant workarounds to the null problem, preferring Kotlin's explicit distinction between nullable and non-nullable types.
  • The article promotes the idea that null can be a useful tool in API design when used within a type-safe system like Kotlin's, leading to clearer and safer code.

Null is your friend, not a mistake

Kotlin Island from Wikimedia by Pavlikhin, CC BY-SA 4.0

I’ve been programming in Java for a long, long time. I’ve learned what it takes to write and maintain big (as in million-lines of code) software in Java and I’ve witnessed industry-wide struggle to avoid and contain the dreaded NullPointerException (NPE) that seemed to plague any reasonably-sized Java codebase. This realization of the danger of the null reference had dawned on the industry way before its inventor, Tony Hoare, had admitted in 2009 that null reference was his “Billion Dollar Mistake”.

It was not that obvious back in 1996 when Java 1.0 was released. Let us take a look at just one, now famous, example of a typical Java API from that era: File.list() method. It can be used to list contents of a directory like this:

for (String name : new File("directory").list()) {
    System.out.println(name);
}

It works only if the directory exists. Otherwise it throws an NPE, because list returns null. But who would ever write code like that? Not only list documentation clearly states that it returns null when directory is missing, but modern IDEs flag this particular code with a warning right away.

However, people do these kind of mistakes all the time when programming in Java. By now there is a big body of research showing how it happens. It turns out that most of the time our API functions are not supposed and are not expected by other developers to return null. In corner cases, like the absence of something, it is a convention in Java to return some “null object” (empty collection, unfilled domain object, etc) or, as a lesser evil than returning null, to throw an exception. That is how Files.newDirectoryStream — a modern version of File.list, is designed — no nulls anywhere.

So when null does appear as a result of a function in some special case, as performance optimization, uninitialized reference field, etc, it often catches other code off-guard, unprepared to deal with it. Not only it is rare to be having to deal with nulls, but the code you have to write for dealing with nulls in Java is long and verbose:

String[] list = new File("directory").list();
if (list != null) {
    for (String name : list) {
        System.out.println(name);
    }
}

No wonder you’d rather choose not to write it, unless absolutely necessarily (necessity often happens when your customer discovers NPE in production).

Fear of null leads to some extreme practices. There are Java coding styles that forbid null completely, forcing heinous workarounds upon developers. Have you seen a codebase where every domain object implements a special Null interface and must provide manually coded “null object” instance? Lucky if you have not, but I bet you’ve seen Optional wrappers that pollute Java code only for the sake of avoiding nulls.

There are collection-like APIs that forbid null elements out of extra caution and there are members of core Java team that consider support for nulls in Java collection framework to be a mistake. This is extremely sad.

The truth is that the concept of null is not mistake, but Java’s type-system, that considers null to be a member of every type, is. See, "abc" is a valid String in Java and null is a valid String, too, yet you can use all the string methods like substring on the former, but attempts to use them on the latter universally fail at runtime. Is it type-safe? Not really. It is normal when some operations fail at runtime on certain values of a type (like division by zero), but when a value leads to runtime failure of all operations, it shows that the value should not belong to this type in the first place. All those NPEs in Java programs indicate an obvious flaw in Java type-system.

More type-safe programming languages, like Kotlin, fix this flaw by properly incorporating the concept of null into the type system. Adding inspections and warning does help, too, but it is not enough. Clearly, a sound type system must only allow such values of type String that can support its operations, so in Kotlin putting null into a variable of type String is not just a warning, but a type error akin to assigning an integer value 42 to a variable of type String.

Proper null support in type system is a game-changer for API design. There is no reason whatsoever to fear null anymore. Having some functions that return a nullable type String? side-by-side with others that return non-null String is as normal as having some functions that return String side-by-side with others that return Int. They are simply different types with different and safe sets of operations available for them.

Type-safe null is better, more efficient, and less verbose alternative to represent all sorts of “missing results”. Look at Kotlin standard library for inspiration, for example. There is String.toIntOrNull() function that parses a string to an integer, if possible, or returns null if not. It is joy to use. Writing a command-line application that takes an integer parameter and properly complains on its absence is easy:

fun main(args: Array<String>) {
    val id = args.getOrNull(0)?.toIntOrNull() 
        ?: error("id expected")
    // ...
}

Embrace null in your API design. Null is your friend with Kotlin. There is no reason to fear it and no reason to work around it with “null object” pattern or wrappers, let alone with exceptions. Proper use of nulls in your APIs results in more readable and safer code, free from boilerplate.

Further reading

If you liked the topic and want to know more details from the standpoint of language design then consider reading the companion to this story— “Dealing with absence of value”.

Kotlin
Java
Null
Recommended from ReadMedium