Optional

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 Optional worse than null
  • Integrate Optional with streams via flatMap

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

  1. Optional as field or parameter — serializes poorly, adds noise; use nullable or overloads.
  2. Optional.of(null) — immediate NPE.
  3. orElse(expensive()) — expensive always runs; use orElseGet.
  4. get() without check — same as unchecked null dereference.
  5. Collections of Optional — model absence by omitting element or use a sum type / sealed hierarchy instead.
  6. Using Optional everywhere “to be safe” — obscures APIs; reserve for returns where absence is expected.

Practice checkpoint

  1. Method returns null when not found — refactor signature? Answer: return Optional<T> and Optional.ofNullable internally.

  2. Chain: user → optional address → optional zip — one expression? Answer: user.flatMap(User::address).map(Address::zip) (with methods returning Optional).

  3. Default value from DB only if empty — orElse or orElseGet? Answer: orElseGet(() -> repo.load(id)).

  4. 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/