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.
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:
In short, it removes boilerplate code and lets the developer focus on the “business” code i.e. foo.doSomething().
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.
NOTE | The 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:
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 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.
NOTE | More 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?
There are some creative solutions available to handle null values beside Optional.
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:
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:
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:
NOTE | FindBugs nullability annotations are marked deprecated in the latest version, and point to the JSR 305. |
Provider | Nullable annotation | Non-nullable annotation |
JSR 305 | javax.annotation.CheckForNull | javax.annotation.Nonnull |
FindBugs | edu.umd.cs.findbugs.annotations.CheckForNull | edu.umd.cs.findbugs.annotations.NonNull |
Eclipse | org.eclipse.jdt.annotation.Nullable | org.eclipse.jdt.annotation.NonNull |
IntelliJ IDEA | org.jetbrains.annotations.Nullable | org.jetbrains.annotations.NotNull |
NOTE | Both 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.
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.
NOTE | Kotlin’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.
NOTE | At least IntelliJ IDEA will read Java nullability annotations to translate those into the enhanced type system. |
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
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]