skip to content

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.

java
Optional<User> findById(long id)   // the signature says: this can come back empty

working inside the box

raw =
  1. Optional.of(" ada ")
    Optional" ada "
  2. .map(String::strip)
    ·
  3. .filter(s -> !s.isEmpty())
    ·
  4. .map(String::toUpperCase)
    ·
  5. .orElse("ANONYMOUS")
    ·

a box with a string inside. the pipeline will work on what's inside without taking it out.

step 1/5
pick an input and step through. each function is called only when there's something in the box. once the box is empty, every later step is skipped, and nothing ever touches a null.
Main.java
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()));
}
output
ADA
ANONYMOUS
ANONYMOUS

the whole API on one screen:

methoddoes
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:

java
Optional.of("x").map(s -> Optional.of(s + "!"))       // Optional[Optional[x!]]
Optional.of("x").flatMap(s -> Optional.of(s + "!"))   // Optional[x!]

orElse vs orElseGet

predict the output
java
String fallback() {
    IO.println("fallback() ran");
    return "guest";
}
 
void main() {
    Optional<String> name = Optional.of("ada");
    IO.println(name.orElse(fallback()));
}
Main.java
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));
}
output
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.

stack
main
a
b
none
alsoNone
heap
Optional
value
String
"ada"
Optional
value
String
"bob"
OptionalEMPTY
valuenull
two full boxes are two objects, each with a reference to its value. every empty optional is the same shared instance. (so yes, 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.

instead
private String nickname; // nullable
Optional<String> nickname() { return Optional.ofNullable(nickname); }
click each place. yes means Optional is the right tool, no means there's something simpler, shown underneath.

the anti-patterns

don'ta null check with extra steps
java
Optional<User> u = findById(id);
if (u.isPresent()) {
    send(u.get().email());
} else {
    log.warn("no user " + id);
}
dosay it in one expression
java
findById(id)
    .map(User::email)
    .ifPresentOrElse(this::send,
        () -> log.warn("no user " + id));
keylines
  • Optional is 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 evaluates x. orElseGet only calls it when empty.
  • get() without a check is a hidden NPE. prefer orElseThrow().
  • no Optional fields, parameters, or wrapped collections. and never return a null Optional.