Optional<T> is a container that either holds a value or is empty. Its purpose is
not to eliminate null — it is to move "there might be nothing here" out of your head and
into the type signature, where the compiler and the next reader can both see it.
The problem it solves
record Customer(String name) { }
class Repo {
// What does this return when there is no such customer? Nothing says.
Customer findByIdOld(String id) {
return null;
}
// This one tells you, in the signature, before you write a line
Optional<Customer> findById(String id) {
return Optional.empty();
}
}
Nothing forces a caller of findByIdOld to consider the null case. The Javadoc might
mention it; nobody reads it, and the NullPointerException arrives in production. The
second signature makes the caller acknowledge the empty case to get at the value at all.
Creating one
Optional<String> present = Optional.of("value"); // throws if you pass null
Optional<String> empty = Optional.empty();
Optional<String> maybe = Optional.ofNullable(null); // empty if null, otherwise present
System.out.println(present.isPresent()); // true
System.out.println(empty.isEmpty()); // true
System.out.println(maybe.isEmpty()); // true
Optional.of versus ofNullable matters: of throws on
null, which is what you want when the value genuinely cannot be null and a null means a
bug. Use ofNullable at the boundary with code that might hand you one.
Getting the value out
There is a right way and a wrong way, and the wrong way is the one that looks most obvious:
class Demo {
String expensiveDefault() {
return "computed"; // pretend this hits a database
}
void run() {
Optional<String> name = Optional.of("Folau");
Optional<String> none = Optional.empty();
// Wrong: get() throws NoSuchElementException on empty — you have swapped
// one exception for another and gained nothing.
System.out.println(name.get());
// Right: say what happens when it is empty
System.out.println(none.orElse("unknown")); // unknown
System.out.println(none.orElseGet(this::expensiveDefault)); // lazy — only if empty
// none.orElseThrow(() -> new IllegalStateException("required"));
name.ifPresent(n -> System.out.println("hello " + n)); // hello Folau
none.ifPresentOrElse(
n -> System.out.println("hello " + n),
() -> System.out.println("nobody here")); // nobody here
}
}
orElse versus orElseGet is a real distinction:
orElse(expensive()) evaluates its argument every time, even when the
Optional has a value, because arguments are evaluated before the call.
orElseGet(() -> expensive()) runs only when empty. Use orElse for
constants and orElseGet for anything computed.
If you are calling get(), you have probably missed the point. Its use
is legitimate only after a check the compiler cannot see, and even then orElseThrow()
with no argument reads better.
Chaining — where it actually pays off
The real benefit is not the null check; it is that map and flatMap let
you keep working without unwrapping:
record Address(String city) { }
record Customer(String name, Address address) { }
class Repo {
Optional<Customer> findById(String id) {
return Optional.of(new Customer("Folau", new Address("Nuku'alofa")));
}
String cityOf(String id) {
return findById(id)
.map(Customer::address) // Optional<Address>
.map(Address::city) // Optional<String>
.map(String::toUpperCase)
.orElse("unknown"); // NUKU'ALOFA
}
}
Any step being empty short-circuits the rest and produces "unknown". The nested-null
version of that method is four if statements deep.
filter works as you would expect, and flatMap is for when the function
itself returns an Optional — without it you would get a
Optional<Optional<T>>:
Optional<String> name = Optional.of("Folau");
System.out.println(name.filter(n -> n.length() > 3).isPresent()); // true
System.out.println(name.filter(n -> n.length() > 30).isPresent()); // false
Optional<String> nested = name.flatMap(n -> Optional.of(n.toLowerCase()));
System.out.println(nested.orElse("none")); // folau
System.out.println(name.stream().count()); // 1 — bridges to streams
or, and combining two sources
A pattern that comes up constantly: try one lookup, fall back to another, and only then give up.
or chains Optionals without unwrapping any of them:
class Lookup {
Optional<String> fromCache(String key) { return Optional.empty(); }
Optional<String> fromDatabase(String key) { return Optional.of("from-db"); }
String resolve(String key) {
return fromCache(key)
.or(() -> fromDatabase(key)) // only called if the cache missed
.orElse("not found"); // from-db
}
}
Like orElseGet, the supplier is lazy — the database is not touched when the cache
hits. Written with if statements this is six lines and an intermediate variable.
It is not a null check with extra steps
A common misuse is wrapping a value purely to test it, which is longer than the thing it replaced:
String input = "value";
// Pointless — you already have the value in hand
if (Optional.ofNullable(input).isPresent()) {
System.out.println(input.length());
}
// Just check it
if (input != null) {
System.out.println(input.length());
}
Optional earns its place at an API boundary — as the return type of a method whose
caller genuinely might get nothing. Inside a method, where you can see the assignment three lines up,
a plain null check is clearer and cheaper. Reaching for isPresent() followed by
get() is the same mistake in a different shape: that pair is an if statement
wearing a costume, and map, filter or ifPresent is what the
type was built for.
A real one
From the console bank app this site uses for examples — the store returns an
Optional rather than null:
public Optional<T> findFirst(Predicate<T> test) {
return findAll().stream().filter(test).findFirst();
}
public Optional<T> findById(long id) {
return findFirst(item -> idOf(item) == id);
}
And the sign-in that consumes it. This is the clearest argument for Optional in the
whole app, and it is a security argument rather than a null-safety one:
public User signIn(String email, String password) {
Optional<User> found = userStore.findByEmail(email);
// The whole check reads as one expression: present, and the password matches.
// Optional.filter keeps the value only if the test passes, so an unknown email and a wrong
// password both arrive at the same empty Optional — no branch, and no way to leak which.
return found.filter(user -> user.passwordMatches(password == null ? "" : password))
.orElseThrow(() -> new AuthenticationException("Invalid email or password."));
}
Written with if statements, the "no such user" and "wrong password" branches are
separate, and it is easy for one of them to produce a different message or return a little sooner —
which tells an attacker which emails are registered. Because filter collapses both into
the same empty Optional, there is only one exit and only one message.
The three places Optional does not belong
Optional was designed for one job — a return type that might have nothing — and using it elsewhere causes more problems than it solves.
1. Not as a field. It is not serialisable, it adds an object per field, and a
null Optional field is the worst of both worlds:
class Bad {
private Optional<String> nickname; // no — and this can itself be null
}
class Good {
private String nickname; // may be null internally
Optional<String> getNickname() { // Optional at the boundary, where callers see it
return Optional.ofNullable(nickname);
}
}
2. Not as a method parameter. It forces every caller to wrap, and they can still
pass null. Use an overload instead — one method with the parameter, one without.
3. Not for collections. An empty List already means "nothing here".
Optional<List<T>> gives callers two empty cases to handle instead of one.
Where you will meet it
List<String> names = List.of("Ana", "Bo");
System.out.println(names.stream().filter(n -> n.startsWith("B")).findFirst().orElse("none"));
System.out.println(names.stream().max(Comparator.naturalOrder()).orElse("none"));
Map<String, Integer> ages = Map.of("Ana", 30);
System.out.println(Optional.ofNullable(ages.get("Bo")).orElse(0)); // 0
The stream API returns Optional from every operation that might find nothing, which
is where most people first encounter it. Note that Map.get still returns
null — for compatibility — so wrapping it, or using getOrDefault, is on
you.
Next
Next: forEach — iterating a collection the functional way, and the two things it cannot do that a loop can.