Is Java Optional Only Smoke and Mirrors?

By: nicolas
  |  March 17, 2023
Is Java Optional Only Smoke and Mirrors?

There are a lot of misconceptions in the software development world. Today we are going to address this one:

“Java 8, for example, introduced the Optional class. It’s a container that may hold a value of some type, or nothing. In other words, it’s a special case of a Monad, known in Haskell as the Maybe Monad.

You can now stop using null in Java.

You can now say goodbye to NullPointerExceptions.”

https://medium.com/@nicolopigna/oops-i-fpd-again-14a3aecbbb98

I won’t go into the Monad thing – at least explicitly, but I challenge the “goodbye to NullPointerException” part.

Using Optional

Java 8 did indeed introduce the concept of Optional. An instance of Optional can be created in the following way:

// Creates an empty Optional 
Optional empty = Optional.empty(); 
 
// Creates a non-empty optional 
Optional foo = Optional.of(new Foo());

There are now several ways to use the newly-created Optional variable.

Developers coming from an imperative programming background will probably use it this way:

Optional optional = ... // Create Optional 
 
if (optional.isPresent()) { 
    Foo foo = optional.get(); 
    foo.doSomething(); 
}

But Optional offers a better alternative. Thanks to lambdas and functional programming idioms creeping into the Java world since Java 8, it’s possible to rewrite the above snippet:

Optional optional = ... // Create Optional 
 
optional.ifPresent(foo -> foo.doSomething()); // Using lambdas 
optional.ifPresent(Foo::doSomething); // Using method reference

This approach offers two main benefits:

  1. It hides the if inside the method call
  2. It hides the unwrapping as well

In short, it removes boilerplate code and lets the developer focus on the “business” code i.e. foo.doSomething().

The power of Optional

Additionally, Optional allows for method call chaining.

Consider the following Foo class:

public class Foo { 
 
    public Optional getBar() {
        // Return an Optional somehow 
        ... 
    }
} 

From an Optional, I want to call a method on bar if it exists.

Optional optional = ... // Create Optional 
 
optional.ifPresent(foo -> foo.getBar().ifPresent(bar -> bar.doSomethingElse())); 
optional.ifPresent(foo -> foo.getBar().ifPresent(Bar::doSomethingElse));

By now, the functional approach has become bloated again, in any form.

NOTEThe first lambda cannot be replaced by a method reference because of the method chaining.

From a readability point of view, it’s better to get back to imperative programming again – if only partially.

Optional optional = ... // Create Optional 
 
if (optional.isPresent()) { 
    Foo foo = optional.get(); 
    foo.getBar().ifPresent(Bar::doSomethingElse); 
}

As can be seen, the crux of the matter is to:

  1. First unwrap the Optional<Foo> to get a Foo
  2. Then access the Optional<Bar> through the getter

Once we are able to get the latter, it’s quite straightforward to call ifPresent(). That’s where functional programming transformations can help.

The naive approach is to use map():

Optional madness = optional.map(Foo::getBar);

However, the result is now a nested structure that is even as hard to work with as previously.

Developers familiar with streams and this issue know about the flatMap() method, and how it can transform a List<List<T>> stream into a simple List<T> stream, thus “flattening” the data structure. Good news, Optional also has a flatMap() method that works in exactly the same way:

Optional bar = optional.flatMap(Foo::getBar); 
 
bar.ifPresent(Bar::doSomethingElse);

At this point, one can only be very enthusiastic about Optional and how it will make the code better. No more NullPointerException! Functional programming forever! And yet, this is not as simple as it looks.

The core issue

The assumption that we built everything on is that an Optional instance can wrap either null or a value. Unfortunately, there’s a third alternative:

An Optional can be null.

Of course, that’s evil to the core, but that’s perfectly valid regarding the Java language:

Optional empty = Optional.empty(); 
Optional foo = Optional.of(new Foo()); 
Optional trouble = null;

Nothing prevents a variable from being assigned null, and Optional is a type like any other. Of course, your favorite IDE will probably complain, or even propose that you to fix the issue.

NOTEMore modern languages, such as Scala, suffer from the same problem, with an Option type that can be null.

Yet, there’s no way you can trust third-party code to have been so diligent. Even regarding your own code, using Optional must be done in a consistent manner across all your codebase. That can be an issue if it, or your team, is large enough.

Does that mean we are back to square one?

Alternatives to Optional

There are some creative solutions available to handle null values beside Optional.

The Null type pattern

Before Java 8, one simple way to cope with null was to create a subtype representing Null for a specific type, name it accordingly, override its methods with an empty implementation and make it a singleton. For example, given the Foo class:

public class NullFoo extends Foo { 
 
    private static final NullFoo SINGLETON = new NullFoo(); 
 
    private NullFoo() {} 
 
    public static NullFoo getInstance() {
        return SINGLETON; 
    }
 
    @Override 
    public Optional getBar() { 
        return Optional.empty(); 
    } 
}

It can then be used in the following way:

Foo foo = new Foo(); 
Foo nullFoo = NullFoo.getInstance();

While it’s quite interesting from a design point of view, it’s lacking compared to Optional:

  • It requires writing a lot of code for each custom type, while Optional is generic and can be applied to any type
  • It doesn’t offer functional programming capabilities e.g. map() and flatMap(). While they can be added, it’s involves writing even more code.
  • It doesn’t solve the original issue of Optional as variables can still be initialized with null (or set later).

Annotations to the rescue

The problem caused by null values comes from interacting with external code.

Another alternative to handle null values is through the usage of annotations, one for nullable values, one for non-null ones. For example, Java Specification Request 305 respectively offers @CheckForNull and javax.annotation.Nonnull. They can be used on parameters, methods and packages:

  • Setting it on a parameter is pretty self-explanatory
  • On a method, it applies to the return type
  • While on a package, it sets the default annotation for this package. That means that a parameter/method located in a package will benefit from this annotation if it’s not already annotated.

Here’s a sample:

public class Foo { 
 
    @CheckForNull 
    public Foo doSomethingWith(@Nonnull Foo foo) { 
        // Do something else 
        ... 
    } 
}

Because annotating each method and parameter is pretty annoying,

Unfortunately, JSR 305 is currently dormant while the latest update is from 2006. However, despite its dormant status, there are existing implementations of the JSR, like here and here.

Annotations alone are not enough to help with the better handling of possible null values. One needs some help from static code analyzers, either standalone such as FindBugs, or embedded in IDEs such as IntelliJ IDEA and Eclipse. Each tool provides its own custom annotations package to handle nullability:

NOTEFindBugs nullability annotations are marked deprecated in the latest version, and point to the JSR 305.
ProviderNullable annotationNon-nullable annotation
JSR 305javax.annotation.CheckForNulljavax.annotation.Nonnull
FindBugsedu.umd.cs.findbugs.annotations.CheckForNulledu.umd.cs.findbugs.annotations.NonNull
Eclipseorg.eclipse.jdt.annotation.Nullableorg.eclipse.jdt.annotation.NonNull
IntelliJ IDEAorg.jetbrains.annotations.Nullableorg.jetbrains.annotations.NotNull
NOTEBoth IDEs allow to complete control over annotations. One can also use the “standard” JSR, annotations from the other IDE, one’s own, or even all of them.

The biggest flaw of nullability annotations is that they don’t provide anything on their own. They are just hints, and require a correctly configured static code analyzer to be of any help.

Enhancing the type system

Some languages, such as Kotlin, take another approach by leveraging the type system itself to enforce non-nullability. For every “real” type, there’s one nullable and one not-nullable type.

NOTEKotlin’s compiler is quite advanced regarding type-inference. In the following snippets, types are explicitly written to make code easier to understand for non-Kotlin developers, but are not necessary.

Given a type Baz:

var baz: Baz = Baz() // Can never ever be null
var empty: Baz? = null

Moreover, the compiler knows the difference between nullable and non-nullable types. It will complain is one tries to call a method from a nullable type:

baz.doSomething() // OK, buddy 
empty.doSomething() // Compile-time error!!!

For the second line to compile, one needs to use the safe call operator:

empty?.doSomething()

For value-returning methods, using the safe call operator means the returned type is nullable.

class Baz { 
    fun doSomething(): Unit { 
        // Do something here 
    } 
    
    fun getBar(): Bar = Bar() 
} 
 
var bar: Bar? = empty?.getBar()

Even if the getBar() method returns a non-nullable type, bar can be null because empty might be null. Hence, bar type is nullable – Bar?.

All seems to be perfect in Kotlin world, but there’s one minor caveat. Kotlin reuses a lot of Java libraries. Those libraries do not offer the enhanced type system described above. That means it’s very important to be very cautious regarding interaction with Java code.

NOTEAt least IntelliJ IDEA will read Java nullability annotations to translate those into the enhanced type system.

Conclusion

In this post, we saw how Optional only partially solves the NullPointerException issue, because Optional type variables can still be null. There are other alternatives to handle null values, such as nullability annotations, or even switching to other languages where null handling is part of the type system. However, none of them offer true protection from NullPointerException.

Yet, that doesn’t mean that Optional is of no use. In particular, it really shines in functional programming pipelines introduced by Java 8.

With APM, server health metrics, and error log integration, improve your application performance with Stackify Retrace.  Try your free two week trial today

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]