Divided we Stand: Optional
Our recent article “NULL is Not The Billion Dollar Mistake. A Counter-Rant” got us a lot of reads, controversial comments, and a 50/50 upvote / downvote ratio pretty much everywhere a blog post can be posted and voted on. This was expected.
Objectively, NULL is just a “special” value that has been implemented in a variety of languages and type systems, and in a variety of ways – including perhaps the set of natural numbers (a.k.a. “zero”, the original null – them Romans sure didn’t like that idea).
Or, as Charles Roth has put it adequately in the comments:
Chuckle. Occasionally a mathematics background comes in handy. Now we could argue about whether NULL was “invented” or “discovered”…
Now, Java’s null
is a particularly obnoxious implementation of that “special value” for reasons like:
Compile-time typing vs. runtime typing
// We can assign null to any reference type Object s = null; // Yet, null is of no type at all if (null instanceof Object) throw new Error("Will never happen");
The null literal is even more special
// Nothing can be put in this list, right? List<?> list = new ArrayList<Void>(); // Yet, null can: list.add(null);
Methods are present on the null literal
// This compiles, but does it run? ((Object) null).getClass();
Java 8’s Optional
The introduction of Optional
might have changed everything. Many functional programmers love it so much because the type clearly communicates the cardinality of an attribute. In a way:
// Cardinality 1: Type t1; // Cardinality 0-1: Optional<Type> t01; // Cardinality 0..n: Iterable<Type> tn;
A lot of Java 8’s Optional
‘s interesting history has been dug out by Nicolai Parlog on his excellent blog.
Be sure to check it out: http://blog.codefx.org/tag/optional
In the Java 8 expert groups, Optional
wasn’t an easy decision:
[…] There has been a lot of discussion about [Optional] here and there over the years. I think they mainly amount to two technical problems, plus at least one style/usage issue:
- Some collections allow null elements, which means that you cannot unambiguously use null in its otherwise only reasonable sense of “there’s nothing there”.
- If/when some of these APIs are extended to primitives, there is no value to return in the case of nothing there. The alternative to Optional is to return boxed types, which some people would prefer not to do.
- Some people like the idea of using Optional to allow more fluent APIs. As in
x = s.findFirst().or(valueIfEmpty)
vsif ((x = s.findFirst()) == null) x = valueIfEmpty;
Some people are happy to create an object for the sake of being able to do this. Although sometimes less happy when they realize that Optionalism then starts propagating through their designs, leading toSet<Optional<T>>
’s and so on.It’s hard to win here.
– Doug Lea
Arguably, the main true reason for the JDK to have introduced Optional
is the lack of availability of project valhalla’s specialization in Java 8, which meant that a performant primitive type stream (such as IntStream
) needed some new type like OptionalInt
to encode absent values as returned from IntStream.findAny()
, for instance. For API consistency, such an OptionalInt
from the IntStream
type must be matched by a “similar” Optional
from the Stream
type.
Can Optional be introduced late in a platform?
While Doug’s concerns are certainly valid, there are some other, more significant arguments that make me wary of Optional
(in Java). While Scala developers embrace their awesome Option
type as they have no alternative and hardly ever see any null
reference or NullPointerException
– except when working with some Java libraries – this is not true for Java developers. We have our legacy collections API, which (ab-)uses null
all over the place. Take java.util.Map
, for instance. Map.get()
‘s Javadoc reads:
Returns the value to which the specified key is mapped, or
null
if this map contains no mapping for the key.[…]
If this map permits null values, then a return value of null does not necessarily indicate that the map contains no mapping for the key; it’s also possible that the map explicitly maps the key to null. The containsKey operation may be used to distinguish these two cases.
This is how much of the pre-Java 8 collection API worked, and we’re still using it actively with Java 8, with new APIs such as the Streams API, which makes extensive use of Optional
.
A contrived (and obviously wrong) example:
Map<Integer, List<Integer>> map = Stream.of(1, 1, 2, 3, 5, 8) .collect(Collectors.groupingBy(n -> n % 5)); IntStream.range(0, 5) .mapToObj(map::get) .map(List::size) .forEach(System.out::println);
Boom, NullPointerException
. Can you spot it?
The map
contains remainders of a modulo-5 operation as keys, and the associated, collected dividends as a value.
We then go through all numbers from 0 to 5 (the only possible remainders), extract the list of associated dividends, List::size
them… wait. Oh. Map.get
may return null
.
You’re getting used to the fluent style of Java 8’s new APIs, you’re getting used to the functional and monadic programming style where streams and optional behave similarly, and you may be quickly surprised that anything passed to a Stream.map()
method can be null
.
In fact, if APIs were allowed to be retrofitted, then the Map.get
method might look like this:
public interface Map<K,V> { Optional<V> get(Object key); }
(it probably still wouldn’t because most maps allow for null
values or even keys, which is hard to retrofit)
If we had such a retrofitting, the compiler would be complaining that we have to unwrap Optional
before calling List::size
. We’d fix it and write
IntStream.range(0, 5) .mapToObj(map::get) .map(l -> l.orElse(Collections.emptyList())) .map(List::size) .forEach(System.out::println);
Java’s Crux – Backwards compatibility
Backwards compatibility will lead to a mediocre adoption of Optional
. Some parts of JDK API make use of it, others use null
to encode the absent value. You can never be sure and you always have to remember both possibilities, because you cannot trust a non-Optional
type to be truly “@NotNull
“.
If you prefer using Optional
over null
in your business logic, that’s fine. But you will have to make very sure to apply this strategy thoroughly. Take the following blog post, for instance, which has gotten lots of upvotes on reddit:
http://shekhargulati.com/2015/07/28/day-4-lets-write-null-free-java-code
It inadvertently introduces a new anti-pattern:
public class User { private final String username; private Optional<String> fullname; public User(String username) { this.username = username; this.fullname = Optional.empty(); } public String getUsername() { return username; } public Optional<String> getFullname() { return fullname; } // good--------^^^ // vvvv--------bad public void setFullname(String fullname) { this.fullname = Optional.of(fullname); } }
The domain object establishes an “Optional
opt-in” contract, without opting out of null
entirely. While getFullname()
forces API consumers to reason about the possible absence of a full name, setFullname()
doesn’t accept such an Optional
argument type, but a nullable one. What was meant as a clever convenience will result only in confusion at the consumer site.
The anti-pattern is repeated by Steven Colebourne (who brought us Joda Time and JSR-310) on his blog, calling this a “pragmatic” approach:
public class Address { private final String addressLine; // never null private final String city; // never null private final String postcode; // optional, thus may be null // constructor ensures non-null fields really are non-null // optional field can just be stored directly, as null means optional public Address(String addressLine, String city, String postcode) { this.addressLine = Preconditions.chckNotNull(addressLine); this.city = Preconditions.chckNotNull(city); this.postcode = postcode; } // normal getters public String getAddressLine() { return addressLine; } public String getCity() { return city; } // special getter for optional field public Optional getPostcode() { return Optional.ofNullable(postcode); } // return optional instead of null for business logic methods that may not find a result public static Optional<Address> findAddress(String userInput) { return ... // find the address, returning Optional.empty() if not found } }
- See the full article here: http://blog.joda.org/2015/08/java-se-8-optional-pragmatic-approach.html
Choose your poison
We cannot change the JDK. JDK API are a mix of nullable and Optional
. But we can change our own business logic. Think carefuly before introducing Optional
, as this new optional type – unlike what its name suggests – is an all-or-nothing type. Remember that by introducing Optional
into your code-base, you implicitly assume the following:
// Cardinality 1: Type t1; // Cardinality 0-1: Optional<Type> t01; // Cardinality 0..n: Iterable<Type> tn;
From there on, your code-base should no longer use the simple non-Optional
Type
type for 0-1 cardinalities. Ever.
Reference: | Divided we Stand: Optional from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog. |