Using java.util.Optional everywhere. To be or not to be?

The origin of the question

With its default inspection settings, Intellij Idea complains about usage of java.util.Optional as a field or as a method parameter:

IDE warning in a sample class

But why such usage of Optional is not recommended? Here’s a quote from the SO answer that is cited throughout the internet:

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.

Brian Goetz

Not everyone follows this stance though:

Fact: Optional is not serializable

Authors of JSR 335 (where the class was introduced) explicitly decided to make it non-serializable. The reasons for that decision are not readily available but here’s what I found on the Internet:

This fact somewhat complicates the usage of Optional as a class field. But in reality nothing prevents one from doing so in classes that are not meant to be serialized (which is rarely required these days).

BTW another consequence of this limitation is that Optional can no longer be used as a return type in remote invocation calls that rely on standard Java serialization.

Further reading on the subject:

What if we ignore the warning and use Optional everywhere?

Benefits

More expressive and consistent code

If we use Optional in class fields, method parameters, and method return values, it may help making the code self-documented.

For instance, the below record screams that middleName is not always set:

import java.util.Optional;

public record User(String firstName,
                   Optional<String> middleName,
                   String lastName) {
}

This approach should be used consistently though:

  • If something can be absent, then the type should be Optional<X>
  • Conversely, if something has type X (not Optional<X>) then it should never be absent

Otherwise, it may do more harm than good.

Access to the fluent API of Optional

In the below example we can enjoy using map and orElse:

import java.util.Optional;

public class FluentOptional {
    public static void main(String[] args) {
        User user1 = new User("Linus", Optional.of("Benedict"), "Torvalds");
        User user2 = new User("Martin", Optional.empty(), "Odersky");

        System.out.println(shortName(user1)); // Prints "L. B. Torvalds"
        System.out.println(shortName(user2)); // Prints "M. Odersky"
    }

    private static String shortName(User user) {
        String firstNameSegment = user.firstName().charAt(0) + ". ";
        String middleNameSegment = user.middleName()
                .map(s -> s.charAt(0) + ". ")
                .orElse("");
        String lastname = user.lastName();

        return "%s%s%s".formatted(firstNameSegment, middleNameSegment, lastname);
    }
}

Soul-soothing for a Clear Architecture adept

It may be undesirable to Entity classes to assume any particular serialization mechanism (including as subtle as the standard Java serialization) because according to The Clean Architecture, Entities should be independent of any specific technology.

Drawbacks

Certainly there’re also (potential) negative consequences of the above approach:

  • In case of zealous use, it can backfire and make code less readable, particularly when used in collections: LinkedHashMap<String, Optional<String>> fileId2OverrideNameMap
  • The necessity to disable the warning in IDE
  • Impossibility of some performance-optimizations: one, two

An alternative: Checker Framework

Checker Framework provides compile-time nullness checks:

@Nullable Object   obj;  // might be null
@NonNull  Object nnobj;  // never null
// ...
obj.toString()         // checker warning:  dereference might cause null pointer exception
nnobj = obj;           // checker warning:  nnobj may become null
if (nnobj == null)     // checker warning:  redundant test

However, the framework requires some magic to setup:

javac \
-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
-processorpath $CHECKERFRAMEWORK/checker/dist/checker.jar \
-cp $CHECKERFRAMEWORK/checker/dist/checker-qual.jar \
-processor org.checkerframework.checker.nullness.NullnessChecker

I definitely need more time to assess this promising option.

Conclusion

As usual, there’s no definite answer when to use and when not to use Optional. You need to use your team’s best judgement.

In my personal opinion it is worthwhile to use Optional throughout codebase for method return types and incoming parameters, as well as class fields, as long as it’s used consistently:

  • If something can be absent, then the type should be Optional<X>
  • And vice versa, if something has type X (not Optional<X>) then it should never be absent.
  • Finally, be careful using Optional in collections
comments powered by Disqus