This is java.util.Optional
.
I took out all the comments and did a little reformatting, but this is the entire class. Just around 150 lines managing one nullable field.
Take a minute to read or skim it before moving on.
public final class Optional<T> {
private static final Optional<?> EMPTY =
new Optional<>(null);
private final T value;
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
<T> t = (Optional<T>) EMPTY;
Optionalreturn t;
}
private Optional(T value) {
this.value = value;
}
public static <T> Optional<T> of(T value) {
return new Optional<>(Objects.requireNonNull(value));
}
@SuppressWarnings("unchecked")
public static <T> Optional<T> ofNullable(T value) {
return value == null ? (Optional<T>) EMPTY
: new Optional<>(value);
}
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
public boolean isPresent() {
return value != null;
}
public boolean isEmpty() {
return value == null;
}
public void ifPresent(Consumer<? super T> action) {
if (value != null) {
.accept(value);
action}
}
public void ifPresentOrElse(
<? super T> action,
ConsumerRunnable emptyAction
) {
if (value != null) {
.accept(value);
action} else {
.run();
emptyAction}
}
public Optional<T> filter(Predicate<? super T> predicate) {
.requireNonNull(predicate);
Objectsif (!isPresent()) {
return this;
} else {
return predicate.test(value) ? this : empty();
}
}
public <U> Optional<U> map(
<? super T, ? extends U> mapper
Function) {
.requireNonNull(mapper);
Objectsif (!isPresent()) {
return empty();
} else {
return Optional.ofNullable(mapper.apply(value));
}
}
public <U> Optional<U> flatMap(
<? super T, ? extends Optional<? extends U>> mapper
Function) {
.requireNonNull(mapper);
Objectsif (!isPresent()) {
return empty();
} else {
@SuppressWarnings("unchecked")
<U> r = (Optional<U>) mapper.apply(value);
Optionalreturn Objects.requireNonNull(r);
}
}
public Optional<T> or(
<? extends Optional<? extends T>> supplier
Supplier) {
.requireNonNull(supplier);
Objectsif (isPresent()) {
return this;
} else {
@SuppressWarnings("unchecked")
<T> r = (Optional<T>) supplier.get();
Optionalreturn Objects.requireNonNull(r);
}
}
public Stream<T> stream() {
if (!isPresent()) {
return Stream.empty();
} else {
return Stream.of(value);
}
}
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}
public T orElseThrow() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
public <X extends Throwable> T orElseThrow(
<? extends X> exceptionSupplier
Supplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return obj instanceof Optional<?> other
&& Objects.equals(value, other.value);
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
@Override
public String toString() {
return value != null
? ("Optional[" + value + "]")
: "Optional.empty";
}
}
Optional
existjava.util.Optional
was introduced in Java 8 alongside the Stream
API. Its raison d'etre is to make coders explicitly consider what to do when using methods like findFirst
on a potentially empty Stream
.
// Explicitly throws
int valueOne = list
.stream()
.map(x -> x + 1)
.filter(x -> x % 2 == 0)
.findFirst()
.orElseThrow()
// Explicitly uses a default value
int valueTwo = list
.stream()
.map(x -> x + 1)
.filter(x -> x % 2 == 0)
.findFirst()
.orElse(0)
The deficiency it targets is in the interaction between null
and "method chaining style". When there are so many method calls stacked up, it is hard for people to remember to handle cases like null
return values.
So with streams poised to encourage method chaining, Optional
was needed to make that API not lead to hidden bugs.
Optional
?Nothing really.
The core tension that leads to so much discourse is that there is no way to represent null
in Java's type system. Whether from lived experience or religious fervor, folks tend to be afraid of an unaccounted for null
.
Because Optional
is in the standard library and explicitly represents "absence or presence", it is extremely tempting to just replace every nullable thing with an Optional<T>
.
Doing this can lead to code that sucks, especially if you try to avoid null
for local variables.
// Some might try to use isPresent()/get() to avoid null
<String> nameOpt = f();
Optional<Integer> ageOpt = g();
Optional
if (nameOpt.isPresent() && ageOpt.isPresent()) {
= nameOpt.get();
var name = nameOpt.get();
var age
System.out.println(
+ " is " + age + " years old"
name );
}
// Others might try to map/flatMap.
<String> nameOpt = f();
Optional<Integer> ageOpt = g();
Optional
nameOpt.flatMap(name ->
.map(age -> {
ageOptSystem.out.println(
+ " is " + age + " years old"
name );
}))
// But its questionable what's gained over null.
String name = f().orElse(null);
Integer age = g().orElse(null);
if (name != null && age != null) {
System.out.println(
+ " is " + age + " years old"
name );
}
But this is honestly fine.
Yes, the Optional
will use up more memory and perform a bit worse than the equivalent code with null
. Yes, code written with isPresent
/get
/orElseThrow
or map
/flatMap
can be a bit crusty. Yes, it wasn't intended to be a field or a method parameter. There are a lot of bike sheds to build and "best practices" to get into internet fights over.
But jspecify
is poised to give standard nullability annotations and tooling to augment the type system with them. Project Valhalla is considering giving a way to express null restricted storage. In the fullness of time, the core tension that leads to this "overuse" seems like it will be resolved.
The problem with both Optional
and null
is that they only convey that some data might be absent and not what being absent implies.
Say you were writing a program which had to record peoples' first and last names for legal reasons. Users can still sign up, but they will need to give that information before continuing on to other parts of the app.
Today you might see Optional
being used to represent that.
import java.util.Optional;
Person(
record int id,
<String> firstName,
Optional<String> lastName
Optional) {}
In the near future, maybe a @Nullable
annotation.
import org.jspecify.annotations.Nullable;
Person(
record int id,
@Nullable String firstName,
@Nullable String lastName
) {}
In both cases - null
and an empty Optional
- an absent value implies that you have not been given that information yet.
You can use this to know when to stop a user and ask them for their name.
import java.util.Optional;
Person(
record int id,
<String> firstName,
Optional<String> lastName
Optional) {
boolean shouldAskForInfo() {
return firstName.isEmpty() || lastName.isEmpty();
}
}
import org.jspecify.annotations.Nullable;
Person(
record int id,
@Nullable String firstName,
@Nullable String lastName
) {
boolean shouldAskForInfo() {
return firstName == null || lastName == null;
}
}
Now, consider Madonna. Madonna does not have a last name. If a null
or empty value in the lastName
field means "not provided", you have no way to directly represent "known to not exist."
// Need to ask Bob for his last name still
= new Person(1, "Bob", null);
var bob
// Shouldn't ask Madonna for anything
= new Person(2, "Madonna", null); var madonna
Using an empty string is tempting, but if you do that you will have the same problem that null
currently has. By having a "special" value not expressed in the type system, you are liable to forget to check for that special value.
// Empty string can be a sentinel
= new Person(2, "Madonna", "");
var madonna
// But if you forget that it is special
// you might give Madonna a subpar user experience
= "Hello "
var welcome + person.firstName()
+ " "
+ person.lastName()
+ "!";
// "Hello Madonna !"
// She'll notice. She'll hate you.
The reality of our fictional data model is that we have three distinct cases.
The most convenient tool we have for representing this sort of situation is a sealed interface
.
interface LastName {
sealed NotGiven() implements LastName {}
record DoesNotExist() implements LastName {}
record Given(String value) implements LastName {}
record }
Now when a LastName
has an absent value, we can know whether that is because it doesn't exist or we just haven't been told.
import org.jspecify.annotations.Nullable;
Person(
record int id,
@Nullable String firstName,
LastName lastName) {
boolean shouldAskForInfo() {
return firstName == null
|| lastName instanceof LastName.NotGiven;
}
}
And we can properly represent Madonna.
// Need to ask Bob for his last name still
= new Person(1, "Bob", new LastName.NotGiven());
var bob
// Shouldn't ask Madonna for anything
= new Person(2, "Madonna", new LastName.DoesNotExist());
var madonna
// Joe is all set
= new Person(3, "Joe", new LastName.Given("Shmoe")); var joe
Optional
and null
let you represent exactly 2 possibilities, a sealed hierarchy lets you represent 2 or more possibilities. The reason I'm using the Madonna example is that it is a straw-man where you want to represent 3 distinct possibilities.
My bold claim is that even when there are only 2 possibilities you should still consider making your own class instead of using Optional
or null
.
Both @Nullable String firstName
and Optional<String> firstName
do not directly convey what it means if the data is missing. Its just "absent." The fact that it means you haven't been told is context external to your domain model.
It's a similar problem to primitive obsession. Because null
and Optional
are there and fit the "shape" we want we gravitate to them.
What if instead of that we were to make our own "optional" class.
interface FirstName {
sealed NotGiven() implements FirstName {}
record Given(String value) implements FirstName {}
record }
So here FirstName
is identical in spirit to an Optional<String>
, but with the benefit of us being able to give a name to the situation where there is no value. It's not empty or present, we were either given a first name or we weren't.
With pattern matching you will be able switch over these two situations.
switch (person.firstName()) {
case FirstName.NotGiven _ ->
System.out.println("No first name");
case FirstName.Given(String name) ->
System.out.println("First name is " + name);
}
And part of the reason I put all the code for Optional
at the top was to impress upon you how trivial it would be to add any of those helper methods to a class you made yourself.
interface FirstName {
sealed NotGiven() implements FirstName {
record @Override
public String orElse(String defaultValue) {
return defaultValue;
}
}
Given(String value) implements FirstName {
record @Override
public String orElse(String defaultValue) {
return this.value;
}
}
String orElse(String defaultValue);
}
= person.name().orElse("?"); var name
That's it. That's the thesis.
If you are spending time modeling your domain objects, consider making your own versions of an Optional
class. You can choose names which more align with your domain, adapt to more varied situations, and the boilerplate for doing so is at a historic low.
I will admit that if you have a huge number of fields with potentially missing data this can be more trouble than its worth. I still think its worth considering.