Wednesday, December 14, 2022

Nothing is better than the Optional type. Really. Nothing is better.


JDK 8 introduced the Optional class, a container that is either empty or contains a non-null value.

Core Java, Java Exam, Java Exam Prep, Java Tutorial and Materials, Java Guides, Java Skills, Java Jobs

Optional has numerous problems without countervailing benefits. It does not make your code more correct or robust. There is a real problem that Optional tries to solve, and this article shows a better way to solve it. Therefore, you are better off using a regular and possibly null Java reference rather than Optional.

The web and blogosphere are full of claims that the Optional class solves the problem of null pointer exceptions. This is not true. Changing your code to use the Optional class has the following negative effects:

◉ Optional transforms a NullPointerException into a NoSuchElementException, which still crashes your program.
◉ Optional creates new problems that were not a danger before.
◉ Optional clutters your code.
◉ Optional adds space, time, and coding overhead.

When your code throws a NullPointerException or NoSuchElementException, the underlying logic bug is that you forgot to check all possibilities when processing data. It’s best to use a tool that guarantees you don’t forget. That helps you to understand and fix the underlying problem.

(These criticisms aren’t specific to Java’s implementation of Optional. Other languages that claim to have solved the null pointer problem have also merely transformed it into a different manifestation.)

To be clear, the Optional class isn’t all bad: If you need to use Optional, it defines methods for reducing code clutter when dealing with possibly present data. However, you should still avoid the Optional class.

The rest of this article expands on the points raised above.

Changing the exception that is thrown doesn’t fix the defect or improve the code


Consider the following Cartesian point class with fields x and y (this discussion is equally applicable to getter methods):

class Point { int x; int y; }

Because a Java reference may be null, there is a danger of a NullPointerException whenever you dereference it, as in myPoint.x below.

Point myPoint;
...
... myPoint.x ...

If myPoint is null and does not refer to a real point, then myPoint.x throws a NullPointerException and the program crashes. The following is a way to write this code using Optional:

Point myPoint;
Optional<Point> myOPoint = Optional.ofNullable(myPoint);
...
... myOPoint.get().x ...

If myOPoint does not contain a real point, then myOPoint.get().x throws a NoSuchElementException and the program crashes. This isn’t any better than the original code, because the programmer’s goal is to prevent all crashes, not just NullPointerException crashes!

It is possible to prevent the exception and crash by doing a check first.

if (myPoint != null) {
  ... myPoint.x ...
}

or

if (myOPoint.isPresent()) {
  ... myOPoint.get().x ...
}

Again, the code is very similar and Optional is not superior to using a regular Java reference.

Optional is prone to misuse


Optional is a Java class; therefore, the variable myOPoint of type Optional<Point> might be null. Thus, the expression myOPoint.get() might throw a NullPointerException or a NoSuchElementException! You really need to write the following:

if (myOPoint != null && myOPoint.isPresent()) {
  ... myOPoint.get().x ...
}

You can express complex data using the distinction between a null Optional value, a non-null Optional with data absent, and a non-null Optional with data present, but this is complex and confusing. Alternatively, you could decide to forgo those possibilities and to be careful and disciplined about not letting variables of type Optional be null. However, if you trusted yourself about that, you wouldn’t have had any null pointer exceptions in the first place and you wouldn’t be using Optional.

Optional is a wrapper, so uses of value-sensitive operations are error-prone, including reference equality checks (==), identity hash codes, and synchronization. You need to remember to not use these.

This isn’t a new issue: Back in 2016, Stuart Marks provided a longer list of rules to avoid mistakes in the use of Optional.

Optional clutters your code


With the Optional library, your code is more verbose, as shown in the following examples:

◉ Code for type names: Optional<Point> versus Point
◉ Code for checking a value: myOPoint.isPresent() versus myPoint == null
◉ Code for accessing data: myOPoint.get().x versus myPoint.x

None of these is a deal-breaker alone, but overall, Optional is cumbersome and ugly to use. For some concrete examples, see the Code Project blog’s “Why we should love ‘null’” post and search for the word cumbersome.

Optional introduces overhead


Optional introduces space overhead: An Optional is a separate object that consumes extra memory.

Optional introduces time overhead: Its data must be accessed via an extra indirection, and calling methods on it is more expensive than Java’s efficient test for null.

Optional introduces coding overhead: You must deal with Optional’s incompatibility with existing interfaces that use null, with the fact that it is not serializable, and so on.

The real problem: Remembering to perform checks


A NullPointerException or NoSuchElementException occurs because the programmer forgot to perform a check to see if data is present, via != null or .isPresent(), before trying to use the data.

Many people say that the main benefit of Optional is that with Optional, you are less likely to forget to perform the check. If true, that is good! Nonetheless, it’s not enough to make a problem somewhat less likely, in the few places where Optional is written. It is better to have a guarantee that eliminates the problem everywhere.

One way would be to force the programmer to always perform the check before accessing the data. (This is what some programming languages do, by offering a destructuring or pattern-match operator.) This would result in many redundant checks in places where the check has already been performed or it is not really needed. (As an analogy, think about how some programmers react to checked exceptions, which force the programmer to do a check whether the programmer wants to do it or not.)

A better approach is to have a tool that guarantees that you do not forget to check but that also doesn’t require redundant checks. Luckily, such tools exist; examples include Nullness Checker of the Checker Framework, NullAway, and Infer. (Note: I am the creator of the Checker Framework.)

As an example, consider Nullness Checker. It works at compile time, and it examines every dereference in your program and requires that the receiver is known to be non-null. That could be because you have already checked it or because it was generated by a source that never produces null.

Nullness Checker uses powerful analysis to keep track of whether a reference might be null. By comparison to the use of Optional, this reduces the number of warnings and the number of redundant checks that are needed. By default, Nullness Checker assumes that references are non-null, but you can specify possibly missing data by writing @Nullable, as in the type @Nullable Point.

Writing @Nullable Point is analogous to Optional<Point>, but with the following significant benefits:

◉ There is less clutter, because you write the @Nullable annotation on fields and method signatures—typically not within method bodies.
◉ It is compatible with existing Java code and libraries. There is no need to change your code to call methods of Optional. There is no need to change interfaces and clients to use the Optional type and no need to convert between Optional instances and regular references.
◉ There is no runtime overhead.
◉ You get a compile-time guarantee or a warning, never a runtime crash.
◉ The code is better documented. If Optional is not present on a type, you don’t know whether the programmer forgot it, Optional could not be written because of backward compatibility, or the data is really always present. With the static analysis of Nullness Checker, the annotations are machine-checked at compile time, so the program has @Nullable on every reference that might be null.
◉ You get guarantees about sources of null pointer exceptions, such as partially initialized objects and calls to Map.get, that Optional is not applicable to. It can also express method preconditions, which are useful for fields containing possibly missing data.

Nullness Checker achieves the goal of guaranteeing that you never forget to check for the presence of data, anywhere in your code, in a way that is less disruptive than the use of Optional. Other tools such as NullAway and Infer give similar guarantees with different trade-offs.

Since every programmer error related to null references is possible with Optional, and Optional makes new types of errors possible, programmers need support to avoid making all those errors. The Checker Framework also contains a compile-time Optional Checker that does exactly that, and it is useful if you need to use Optional (such as to interface with a library that uses Optional).

Optional’s handy methods


Although Optional tends to clutter your code, if you need to use Optional, it provides methods that reduce the clutter. Here are two examples.

◉ Its orElse method returns the value if it is present, or else it returns a default value.
◉ Its map method abstracts the pattern by doing the following:
  ◉ It takes as input a value.
  ◉ If the value is null, it returns null.
  ◉ Otherwise, it applies a function to the value and returns the result.

There are libraries that do the exact same things for regular Java references. An example is the Opt class that is distributed with the Checker Framework. For each instance method in Optional, the Opt class includes a static method.

Other methods such as filter and flatMap are described in the API documentation for Optional. These eliminate much of the need for calling Optional.isPresent() and Optional.get(), which is a great benefit. However, they don’t eliminate all the need, and the other disadvantages of Optional remain.

Counterarguments


Not everyone claims that Optional solves the problem underlying null pointer exceptions. For instance, Oracle’s JDK team does not claim this.

A general programming rule is to avoid, as much as possible, the situation that data is not present. This reduces the need to write a type such as Optional<Point> or @Nullable Point. All the arguments in this article continue to hold wherever in your program data might not be present.

Some people suggest (see Stuart Marks’ presentation on bikesheds) that programmers should use Optional sparingly, such as only on method return types and never on fields. If you use Optional less, there is less clutter, overhead, and potential for misuse. However, the only way to eliminate Optional’s problems is to not use it. Furthermore, if you use Optional less, you obtain fewer of its benefits. Null pointer exceptions are important no matter what their source or syntactic form, so the best solution is one that handles every reference in your program, not just some of them.

The main argument for Optional on return types is, “It’s too easy for clients to forget to handle the possibility of a null return value. Optional is ugly and in your face, so a client is less likely to forget to handle the possibility of an empty return value. Everywhere else, programmers should continue to use normal references, which might be null.” By contrast, my suggestion is that programmers should continue to use normal references everywhere in their program but use a tool to ensure that at every possible location—not just at method calls—the program checks against null when needed.

If you find a style of using Optional that solves part of your problems, at an acceptable cost to you, then good for you—use it.

Source: oracle.com

Related Posts

0 comments:

Post a Comment