Null-Checking Done Right with Optionals
Optional monad to better null-checks in your code
NullPointerException. The exception all Java programmers most face in a lifetime. The act of handling null values may be stressful. All those if-elses scattered everywhere in your codebase. Fortunately, you can reduce your null-checking boilerplate code using the Optional class for most of the cases.
The Optional API was first introduced as part of the Java Development Kit 8, and enhanced in subsequent versions of the JDK. It is a container that let us handle value that might not exists (monads!). Even though monads may sound scary, you do not need to know what it means to understand how the Optional API works.
It is also not a silver bullet, and will not resolve all null problems you have in your code. Instead, if used in a correct manner, it helps us to build a more robust API and error-free code.
In this article we’re gonna talk about the do’s and don’ts while using it.
The Optional API
The Optional class is a monadic container that may or may not contain a value inside of it. That's it.
In essence, Optional is a wrapper class that contains a pointer to some other object (present) or to nothing (absent). Another way to look at it is as a way to provide a type-level solution for representing optional values instead of null references.
Although it may sound reasonable to use it everywhere and get rid of nulls, the Optional API was designed to help us handle function return values, as mentioned Brian Goetz:
Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result", and using null for such was overwhelmingly likely to cause errors.
For this reason, it is not recommended that you use it to remove all null check code you have in your program (you can do it at your own risk).
The Seven Rules of Optional
As we now know that Java's Optional shouldn't be used everywhere, there are some rule we are suggested to follow. In fact, Stuart Marks defines seven rules to follow when using Optionals [3]:
- Never, ever, use null for an Optional variable or return value;
- Never use Optional.get() unless you prove that the Optional is present;
- Prefer alternatives to Optional.isPresent() and Optional.get();
- It's generally a bad idea to create and Optional for the specific purpose of chaining methods from it to get a value;
- If an Optional chain has a nested Optional chain, or has an intermediate result of Optional<Optional<T>>, it's probably too complex;
- Avoid using Optional in fields, methods parameters, and collections;
- Avoid using identity-sensitive operations on Optionals (e.g. serializable).
The Do's and Don'ts
As any API, there is the proper manner of using it — and the wrong way too. Here are some tips to keep in mind while handling null values with Optionals:
Instantiation
Optional's core idea is ease the handling of functions' return values, and so one should instantiate an Optional when returning a value that may or may not exist:
You should be aware that for values that might be null, you should call Optional::ofNullable
instead of Optional::of
, which would throw a NullPointerException
[1].
Nonetheless, You should never use Optional as parameters of functions and constructors, neither just for the sake of chaining methods to get a value thereafter.
Optional as Class Attributes
It seems reasonable to use Optional to indicate that an object's attribute may be null. The problem here is that you shouldn't do it. The Garbage Collector overhead and memory consumption is too high for very little benefit.
Instead, you might use Optionals as getters' return values. It guarantees you are expliciting your API may or may not return missing values. Also, the underlying business logic doesn't suffer from performance overhead while checking for nulls.
The caller will call the getter, interpret the result, and then move on. The JVM handles these short-lived objects well [2].
One thing you should keep in mind is that Optional is not Serializable. If your domain object could be Serializable, you couldn't achieve that if any of its fields is Optional [2][3]. This is one more reason to not use it as class attributes.
Using Optional Values
This is the holy grail of Optional values. The Optional monad gives us some very useful methods to handle these values. The point is that the API also has some methods that we should avoid using.
Let's say we create a service to authenticate a user in the system. We must validate if the user doesn't already exists. If not, we create a new one and return to the client.
You shouldn't use Optional::isPresent
and Optional::get
together as they doesn't help you removing if-elses from your code. Also, this way you're running away from the declarative programming's power that Optional brings to us.
Also, using Optional::get
throws NoSuchElementException
if value is absent. Thus using Optional::isPresent
alongside isn't any better than checking for null.
Because of its fluent interface, Optionals are also great when handling streams. We can use map
and orElse
in the while processing the stream.
A point of attention here: orElse
returns T for Optional<T> when the value is absent. You pass a function that creates that value, it is going to call it regardless of the value being absent or not.
Instead, if you doesn't have an object to pass to orElse
, you should use orElseGet
and pass a supplier that will create the object afterwards.
This way, you shouldn't pass functions to orElse
, specially when that functions make IO calls (external APIs, database, filesystem). This can lead to unnecessary performance overhead and unknown side-effects [4].
We can use Optional::filter
as well. If the value wrapped inside the optional is absent, it returns empty; otherwise it applies the predicate to the value.
It makes your code clearer and understandable.
Method Chaining
It is important for you to know that you shouldn't create optionals just for the sake of chaining methods. Aside from the fact that it might make the code less clear, it adds GC overhead and memory consumption for nothing.
Null values aren't bad if they're controlled. The same is for null-checks.
New Features from JDK 9
The JDK 9 brought some nice features to the Optional type, such as pattern matching-ish with ifPresentOrElse
and stream
!
Optional::ifPresentOrElse
As mentioned above, this method brings something like a pattern matching for Java Optional. Suggested by the method’s name, it's a bifunction that receives a function to apply when the value is present; and a function to execute when it's absent.
It works in a similar way to map
+ orElse
. It depends on your preference to use one or another.
Optional::stream
This one is neat. Optional::stream
returns a stream (aha!) with one element if the value is present; otherwise it returns an empty stream. It is handy while processing multiple values that may not exist.
Also, it helps us to reduce the length of chained methods in a stream by replacing filter
and map
for flatMap(Optional::stream)
.
Optional::or
This method works similar to Optional::orElseGet
, which receives a supplier to produce a return value. However, unlike orElseGet
, or
returns an Optional describing its value, otherwise it returns an Optional produced by the supplying function.
This is handy when you have an optional value that, if absent, you must get a value somewhere else that also might not exists.
Conclusion
Even though it is an underused feature of the JDK, the Optional class brings the power to handle absent values in a streamlined data flow, with its fluent API and declarative programming. It is not an antidote for all null checks and null values in your code base. Nonetheless it helps us to build clearer APIs and error-free code.
You also must be aware that, if misused, it can lead to bigger problems in your code base. There are some situations where you cannot use it, such as inside of Serializable classes.
Keep in mind that Optional isn't the only solution regarding monadic containers for absent values. Vavr's Option interface is really good. It has a similar API and, to whose this might be interesting, it extends Serializable [6][7]. In addition to that, Vavr has some good monads for error handling too.
That's all, folks! I hope you enjoyed it and can get improve the usage of Optional values.
Cheers.
References
[1] Optional javadoc: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Optional.html
[2] Java SE 8 Optional, a pragmatic approach: https://blog.joda.org/2015/08/java-se-8-optional-pragmatic-approach.html
[3] Optional by Stuart Marks: https://www.youtube.com/watch?v=fBYhtvY19xA
[4] Guide to Java 8 Optional: https://www.baeldung.com/java-optional
[5] Java 8 Optional In Depth: https://mkyong.com/java8/java-8-optional-in-depth/
[6] Option javadoc (Vavr 0.9.2 API): https://www.javadoc.io/doc/io.vavr/vavr/0.9.2/io/vavr/control/Option.html
[7] Using Java Optional vs. Vavr Option: https://dzone.com/articles/using-java-optional-vs-vavr-option