Friends of OpenJDK Today

Handling Null: Optional and Nullable Types

April 04, 2022

Author(s)

  • Nicolas Frankel

    Nicolas is a developer advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). ... Learn more

Java has long been infamous for its NullPointerException. The reason for the NPE is calling a method or accessing an attribute of an object that has not been initialized.

var value = foo.getBar().getBaz().toLowerCase();

Running this snippet may result in something like the following:

Exception in thread "main" java.lang.NullPointerException
  at ch.frankel.blog.NpeSample.main(NpeSample.java:10)

At this point, you have no clue which part is null in the call chain: foo, or the value returned by getBar() or getBaz()?

In the latest versions of the JVM, the language designers improved the situation. On JVM 14, you can activate "helpful" NPEs with the -XX:+ShowCodeDetailsInExceptionMessages flag. Running the same snippet shows which part is null:

Exception in thread "main" java.lang.NullPointerException: 
  Cannot invoke "String.toLowerCase()" because the return value of 
"ch.frankel.blog.Bar.getBaz()" is null
  at  ch.frankel.blog.NpeSample.main(NpeSample.java:10)

On JVM 15, it becomes the default behavior: you don't need a specific flag.

Handling NullPointerException

In the above snippet, the developer assumed that every part had been initialized.

Displaying the null part helps debug and debunk wrong assumptions.

However, it doesn't solve the root cause: we need to handle the null value somehow.

For that, we need to resort to defensive programming:

String value = null;
if (foo != null) {
    var bar = foo.getBar();
    if (bar != null) {
        baz = bar.getBaz()
        if (baz != null) {
            value = baz.toLowerCase();
        }
    }
}

It fixes the problem but is far from the best developer experience - to say the least:

  1. Developers need to be careful about their coding practice
  2. The pattern makes the code harder to read.

The Option wrapper type

On the JVM, Scala's Option was the first attempt that I'm aware of of a sane null handling approach, even if the concept is baked into the foundations of Functional Programming. The concept behind Option is indeed quite simple: it's a wrapper around a value that can potentially be null.

You can call type-dependent methods on the object inside the wrapper, and the wrapper will act as a filter. Because Option has its methods, we need a pass-through function that works on the wrapped type: this function is called map() in Scala (as well as in several other languages). It translates in code as:

def map[B](f: A => B): Option[B] = if (isEmpty) None else Some(f(this.get))

If the wrapper is empty, i.e., contains a null value, return an empty wrapper; if it's not, call the passed function on the underlying value and return a wrapper that wraps the result.

Since Java 8, the JDK offers a wrapper type named Optional. With it, we can rewrite the above null-checking code as:

var option = Optional.ofNullable(foo)
    .map(Foo::getBar)
    .map(Bar::getBaz)
    .map(String::toLowerCase);

If any of the values in the call chain is null, option is null. Otherwise, it returns the computed value. In any case, gone are the NPEs.

Nullable types

Regardless of the language, the main problem with Option types is its chicken-and-egg nature. To use an Option, you need to be sure it's not null in the first place. Consider the following method:

void print(Optional<String> optional) {
    optional.ifPresent(str -> System.out.println(str));
}

What happens if we execute this code?

Optional<String> optional = null;
print(optional);                       // 1
  1. Oops, back to our familiar NPE

At this point, developers enamored with Option types will tell you that it shouldn't happen, that you shouldn't write code like this, etc. It might be accurate, but it, unfortunately, doesn't solve the issue. To 100% avoid NPEs, we need to get back to defensive programming:

void print(Optional<String> optional) {
    if (optional != null) {
        optional.ifPresent(str -> System.out.println(str));
    }
}

Kotlin chose another route with nullable types and their counterparts, non-nullable types. In Kotlin, each type T has two flavors, a trailing ? hinting that it can be null.

var nullable: String?          // 1
var nonNullable: String        // 2
  1. nullable can be null
  2. nonNullable cannot

The Kotlin compiler knows about it and prevents you from directly calling a function on a reference that could be null.

val nullable: String? = "FooBar"
nullable.toLowerCase()

The above snippet throws an exception at compile-time, as the compiler cannot assert that nullable is not null:

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

The null safe operator, i.e., ?., is very similar to what map does: if the object is null, stop and keep null; if not, proceed with the function call. Let's migrate the code to Kotlin, replacing Optional with a null safe call:

val value = foo?.bar?.baz?.lowercase()

Option or nullable type?

You have no choice if you're using a language where the compiler does not enforce null safety. The question raises only within the scope of languages that do, e.g., Kotlin. Kotlin's standard library doesn't offer an Option type. However, the Arrow library does. Alternatively, you can still use Java's Optional.

But the question still stands: given a choice, shall you use an optional type or a nullable one? The first alternative is a bit more verbose:

val optional: Foo? = Optional.ofNullable(foo)   // 1
                             .map(Foo::bar)
                             .map(Bar::baz)
                             .map(String::lowercase)
                             .orElse(null)

val option = Some(foo).map(Foo::bar)            // 2
                      .map(Bar::baz)
                      .map(String::lowercase)
                      .orNull()
  1. The Java API returns a platform type; you need to set the correct type, which is nullable
  2. Arrow correctly infers the nullable Foo? type

Besides inferring the correct type, Arrow's Option offers:

  • The map() function seen above
  • Other standard functions traditionally associated with monads, e.g., flatMap() and fold()
  • Additional functions

For example, fold() allows to provide two lambdas, one to run when the Option is Some, the other when it's None:

val option = Some(foo).map(Foo::bar)
                      .map(Bar::baz)
                      .map(String::lowercase)
                      .fold(
                        { println("Nothing to print") },
                        { println("Result is $it") }
                      )

Conclusion

If null was a million-dollar mistake, modern engineering practices and languages could cope with it. Compiler-enforced null safety, as found in Kotlin, is a great start.

However, to leverage the full power of Functional Programming, one needs an FP-compliant implementation of Option.

The problem, in this case, is to enforce that Option objects passed are never null.

Kotlin's compiler does it natively, while the Arrow library provides an Option implementation that fulfills the needs of FP programmers.

To go further:

Originally published at A Java Geek on April 3rd, 2022

Topics:

Related Articles

View All
  • Much Ado About Nothing in Java

    Occasionally something in Java pops up that I thought I knew about, but it turns out I didn’t appreciate all the subtle details.

    This was recently the case for “nul”. Before I started using Java, the main programming language I used was C.  This was great for things like operating systems and device drivers because it uses explicit pointers. References to data are through a numerical address that can be manipulated if required.

    Although null might seem like a simple, straightforward concept, there are some edge cases that make its use require a little more thought. I hope this provides you with a better understanding of nothing (null).

    Read More
    February 23, 2021
  • Better Error Handling for Your Spring Boot REST APIs

    One of the things that distinguishes a decent API from one that is a pleasure to work with is robust error handling. Nothing is more frustrating than using some API and getting back cryptic errors where you can only guess why the server is not accepting your request.

    Spring Boot lets you customize the error handling for your application, but there is quite a lot of low-level coding involved if you want to do this correctly.

    Read More
    August 21, 2021
  • Avoiding NullPointerException

    The terrible NullPointerException (NPE for short) is the most frequent Java exception occurring in production, according to a 2016 study. In this article we’ll explore the main techniques to fight it: the self-validating model and the Optional wrapper.

    You should consider upgrading your entity model to either reject a null via self-validation or present the nullable field via a getter that returns Optional. The effort of changing the getters of the core entities in your app is considerable, but along the way, you may find many dormant NPEs.

    Read More
    December 22, 2020

Author(s)

  • Nicolas Frankel

    Nicolas is a developer advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). ... Learn more

Comments (2)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Kenneth Kousen

I know you were just doing an illustration, but you would never really write .or else(null) on am Optional, right? That’s just evil. If I saw that during a code review a shudder would run down my spine. Oof course, as you point out, Java has no way to prevent that.

Great article, btw. 🙂

Nicolas Frankel

No, I wouldn’t, but I wanted to have the equivalent code.

Thanks for your comment and kind words!

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard