optional
a box that might be empty. idioms and anti-patterns.
- chapter
- chapter 04.08
- reading time
- 7 min read
Optional<T> is a box that holds one value or nothing. it's for return types where "no result" is a normal answer, so the caller can't forget about the empty case.
you work inside the box with map, filter and flatMap, and open it once at the end with orElse, orElseGet or orElseThrow. you don't use it for fields, parameters or collections.
the picture
null is a value that every reference type secretly allows and no type ever admits to. a method returns User, you call .name() on it, and once in a while it was null all along. Tony Hoare, who added null references to ALGOL W in 1965, later called it his billion-dollar mistake.
Optional makes "might be missing" part of the type. a parcel that may or may not have something in it: you can leave instructions for whatever's inside ("if there's something, gift-wrap it") without opening it, and only at the very end decide what to do if it turns out to be empty.
Optional<User> findById(long id) // the signature says: this can come back emptyworking inside the box
- Optional.of(" ada ")Optional" ada "
- .map(String::strip)·
- .filter(s -> !s.isEmpty())·
- .map(String::toUpperCase)·
- .orElse("ANONYMOUS")·
a box with a string inside. the pipeline will work on what's inside without taking it out.
null.import java.util.Optional;
String greet(Optional<String> raw) {
return raw.map(String::strip)
.filter(s -> !s.isEmpty())
.map(String::toUpperCase)
.orElse("ANONYMOUS");
}
void main() {
IO.println(greet(Optional.of(" ada ")));
IO.println(greet(Optional.of(" ")));
IO.println(greet(Optional.empty()));
}ADA ANONYMOUS ANONYMOUS
the whole API on one screen:
| method | does |
|---|---|
Optional.of(x) | a full box. throws an NPE if x is null |
Optional.ofNullable(x) | full if x isn't null, empty if it is. the bridge from null-land |
Optional.empty() | the empty box |
map(f) | apply f to the value, if any. a null result becomes empty |
flatMap(f) | same, when f itself returns an Optional (so you don't get a box in a box) |
filter(p) | keep the value only if p says yes |
or(() -> other) java 9+ | if empty, try another Optional |
orElse(x) | the value, or x |
orElseGet(() -> x) | the value, or compute x only now |
orElseThrow() java 10+ | the value, or NoSuchElementException |
orElseThrow(() -> new X()) | the value, or your exception |
ifPresent(c), ifPresentOrElse(c, r) java 9+ | do something with it, or something else |
isPresent(), isEmpty() java 11+ | ask. (usually a sign you could use one of the above) |
stream() java 9+ | a stream of zero or one elements, for flatMap in streams |
map vs flatMap is the one that trips people. if the function itself returns an Optional, map wraps it again:
Optional.of("x").map(s -> Optional.of(s + "!")) // Optional[Optional[x!]]
Optional.of("x").flatMap(s -> Optional.of(s + "!")) // Optional[x!]orElse vs orElseGet
String fallback() {
IO.println("fallback() ran");
return "guest";
}
void main() {
Optional<String> name = Optional.of("ada");
IO.println(name.orElse(fallback()));
}import java.util.Optional;
String fallback() {
IO.println(" fallback() ran");
return "guest";
}
void main() {
Optional<String> name = Optional.of("ada");
IO.println("orElse:");
IO.println(name.orElse(fallback()));
IO.println("orElseGet:");
IO.println(name.orElseGet(this::fallback));
}orElse: fallback() ran ada orElseGet: ada
for a constant (orElse("guest"), orElse(List.of())) it doesn't matter. for anything that does work (a database call, building a big object, logging) use orElseGet.
what's in memory
an Optional is a real object with one field, value. Optional.of allocates one. Optional.empty() doesn't: there's a single shared EMPTY instance in the JDK, and every empty optional is that one object.
Optional.empty() == Optional.empty() is true, but don't rely on == for optionals: they're value-based, compare them with equals.)that box costs 16 bytes and a pointer hop. for a return value that's nothing (and the JIT often removes it entirely when it doesn't escape). for a field in a million objects, it's 16 MB and a cache miss each, which is one of the reasons below.
where it belongs
the field can still be null (now there are three states), it costs an extra object per instance, and Optional isn't Serializable.
the anti-patterns
Optional<User> u = findById(id);
if (u.isPresent()) {
send(u.get().email());
} else {
log.warn("no user " + id);
}findById(id)
.map(User::email)
.ifPresentOrElse(this::send,
() -> log.warn("no user " + id));Optionalis a return type that says "might be empty". that's its job.- work inside the box with
map,filter,flatMap. open it once at the end. orElse(x)always evaluatesx.orElseGetonly calls it when empty.get()without a check is a hidden NPE. preferorElseThrow().- no Optional fields, parameters, or wrapped collections. and never return a null Optional.