Learning goals
By the end of this chapter you should be able to:
- Use
Optional<T>to model “value may be absent” in return types — not everywhere - Chain
map,flatMap,filter,orElse,orElseGet,orElseThrow - Avoid the anti-patterns that make
Optionalworse than null - Integrate
Optionalwith streams viaflatMap
Why Optional exists
Null references cause NullPointerException — often far from the missing value. Optional<T> is a container that either holds a non-null value or is empty.
public Optional<User> findById(String id) {
User u = cache.get(id);
return Optional.ofNullable(u);
}
Intended use: return type when absence is normal. Not for fields, method parameters, or collections (use empty collection instead).
Creating Optionals
Optional<String> empty = Optional.empty();
Optional<String> present = Optional.of("hello"); // null → NPE
Optional<String> maybe = Optional.ofNullable(getName()); // null → empty
Never wrap a literal null in Optional.of. ofNullable is the bridge from legacy nullable returns.
Transforming values
Optional<String> email = findUser(id)
.map(User::email)
.map(String::toLowerCase)
.filter(e -> e.endsWith("@corp.com"));
flatMap when the next step returns Optional:
Optional<String> city = findUser(id)
.flatMap(User::primaryAddress)
.map(Address::city);
Without flatMap, you get Optional<Optional<String>>.
Unwrapping with defaults
String name = optionalName.orElse("Anonymous");
String name2 = optionalName.orElseGet(() -> expensiveLookup());
User user = findUser(id)
.orElseThrow(() -> new NotFoundException(id));
orElse(x) always evaluates x — even if optional is present. Use orElseGet(Supplier) when default is expensive.
Java 9+ additions:
optional.ifPresent(System.out::println);
optional.ifPresentOrElse(v -> log(v), () -> log("missing"));
Optional<String> withDefault = optional.or(() -> fallbackOptional);
Optional in conditionals
Prefer functional chain over isPresent() + get:
// Avoid
if (opt.isPresent()) {
use(opt.get());
}
// Prefer
opt.ifPresent(this::use);
Pattern matching (Java 21+) can destructure when combined with other types — for plain Optional, ifPresent / orElseThrow remain idiomatic.
Optional and streams
List<String> cities = users.stream()
.map(User::primaryAddress)
.flatMap(Optional::stream) // Java 9+
.map(Address::city)
.toList();
Optional.stream() — empty → empty stream; present → single-element stream.
Common mistakes
Optionalas field or parameter — serializes poorly, adds noise; use nullable or overloads.Optional.of(null)— immediate NPE.orElse(expensive())— expensive always runs; useorElseGet.get()without check — same as unchecked null dereference.- Collections of Optional — model absence by omitting element or use a sum type / sealed hierarchy instead.
- Using Optional everywhere “to be safe” — obscures APIs; reserve for returns where absence is expected.
Practice checkpoint
-
Method returns null when not found — refactor signature? Answer: return
Optional<T>andOptional.ofNullableinternally. -
Chain: user → optional address → optional zip — one expression? Answer:
user.flatMap(User::address).map(Address::zip)(with methods returning Optional). -
Default value from DB only if empty —
orElseororElseGet? Answer:orElseGet(() -> repo.load(id)). -
Stream of
Optional<String>to flat list of strings? Answer:.flatMap(Optional::stream).
Interview angles
- Not a serializable field — designed for return values; Joshua Bloch guidance.
- vs null: documents contract in type system; still requires discipline at call site.
- Performance: optional allocation overhead — negligible in business logic; do not use in hot tight loops without measurement.
- Three-valued logic: SQL NULL ≠ Java null ≠ Optional.empty() — mapping layers need explicit rules.
Next: /java/learn/modern-java/records-sealed-pattern-matching/