W3docs

Java Optional

Express the possible absence of a value in Java with Optional, and avoid NullPointerException by design.

Java Optional

Optional<T> is a container that holds either a value of type T or nothing — and tells you which, at the type level, so the compiler can force you to handle the absent case. It was added in Java 8 alongside streams, and the two are designed to fit together: findFirst, findAny, min, max, reduce all return Optional<T> precisely because the answer might not exist, and the API gives you fluent ways to keep computing without ever writing if (x != null).

Optional is not a null replacement everywhere, and the JDK is opinionated about where it belongs. This chapter walks the API end to end, then the three places Optional is the wrong call.

Constructing an Optional

Three constructors, each with a precise meaning:

Optional<String> a = Optional.of("hello");           // present; null arg throws NPE
Optional<String> b = Optional.empty();                // absent
Optional<String> c = Optional.ofNullable(maybeNull);  // present if non-null, else empty

The split matters. Optional.of(x) is the assertion "this value is definitely here" — if you pass null it throws NullPointerException immediately, which is what you want (a bug surfaced at the source, not three frames downstream). Optional.ofNullable(x) is the adaptor you wrap around a legacy API that returns null for "absent."

You almost never construct an Optional by hand inside a stream pipeline — terminals like findFirst and Collectors.maxBy produce them for you.

Asking whether a value is present

The two queries:

Optional<String> opt = lookup(id);
boolean has = opt.isPresent();      // true if a value is held
boolean none = opt.isEmpty();        // Java 11+ -- the opposite of isPresent

You'll see these in production code, but they are usually a code smell: most code that calls isPresent then get would read better as one of the operate-on-it methods below. The query methods are for boundary code where you really do need a boolean — a guard clause, a route decision, a logged-warning branch.

Reading the value safely

The wrong way:

String name = opt.get();   // throws NoSuchElementException if empty

opt.get() is the unchecked read. It's how you turn an Optional back into a value and a runtime exception, exactly what the type was supposed to prevent. Use it only after you've proved the optional is present (or after findFirst().orElseThrow() from a pipeline where empty would be a programmer bug, not an expected case).

The right ways, in order of preference:

String name1 = opt.orElse(\"anonymous\");                        // default value
String name2 = opt.orElseGet(() -> expensiveDefault());          // lazy default
String name3 = opt.orElseThrow();                                 // NoSuchElementException
String name4 = opt.orElseThrow(() -> new MyDomainError(id));      // custom exception
  • orElse(value) — supply a default. The value is always evaluated, even when the optional is present, so don't pass an expensive expression.
  • orElseGet(supplier) — supply a default lazily. The supplier runs only when the optional is empty. Use this for any default that costs more than a literal.
  • orElseThrow() — throw NoSuchElementException if absent. The Java 10+ no-arg form is the modern equivalent of opt.get() when "this absolutely should be present" is the only sensible interpretation at the call site.
  • orElseThrow(supplier) — throw a domain-specific exception. The standard way to translate "absent" into "404 not found."

Transforming the value — map

If the optional is present, apply a function; otherwise stay empty:

Optional<String> upper = opt.map(String::toUpperCase);
Optional<Integer> len   = opt.map(String::length);

The signature is Optional<T>.map(Function<T, R>) -> Optional<R>. The function only runs when a value is present — there's no null check, no if, and no else. This is the operation that makes Optional worth its character count: most chains of "if non-null, do this; if non-null, then do this" collapse into a .map(...).map(...).map(...).

There's a special case the JDK quietly handles: if your map function returns null (because it wraps a legacy API that returns null for "no result"), the resulting Optional is empty() — not Optional.of(null).

Composing optionals — flatMap

When the mapping function itself returns an Optional, map would produce Optional<Optional<T>>. flatMap flattens it:

record User(String id, Optional<Address> address) {}
record Address(String city) {}

Optional<String> city = userById(id)
    .flatMap(User::address)        // Optional<Address>
    .map(Address::city);            // Optional<String>

flatMap is the operation that lets you chain several lookups, each of which can fail, into a single pipeline. Both fail-cases collapse to Optional.empty() at the end, and the consumer handles them once with orElse / orElseThrow.

Filtering — filter

Tests the value against a Predicate<T>; returns the same optional if it passes, empty() if it doesn't:

Optional<String> nonBlank = opt.filter(s -> !s.isBlank());
Optional<Integer> positive = numberOpt.filter(n -> n > 0);

Acts as a guard inside the optional pipeline. Useful when the question is "I have a value, but is it the right value to keep going with?"

Side effects — ifPresent, ifPresentOrElse

Run code only when the value is present:

opt.ifPresent(name -> log.info(\"hello, {}\", name));

Or run one branch when present and a different one when empty (Java 9+):

opt.ifPresentOrElse(
    name -> log.info(\"hello, {}\", name),
    () -> log.warn(\"no name on the request\"));

These are the right way to express "do something on the way past." They replace the if (opt.isPresent()) { use(opt.get()); } pattern entirely.

Bridging to streams — Optional.stream()

(Java 9+) Turns an Optional<T> into a Stream<T> of zero or one elements:

Stream<String> s = opt.stream();

Useful inside flatMap on a Stream<Optional<T>>:

List<String> presentCities = userIds.stream()
    .map(this::userById)           // Stream<Optional<User>>
    .flatMap(Optional::stream)      // Stream<User>     -- empties drop, presents pass through
    .map(User::city)
    .toList();

That replaces filter(Optional::isPresent).map(Optional::get) with a single flatMap(Optional::stream). Same answer, cleaner pipeline.

or — fall back to another Optional

(Java 9+) If empty, use a supplier of another Optional:

Optional<User> u = primaryLookup(id)
    .or(() -> fallbackLookup(id))
    .or(() -> Optional.of(User.anonymous()));

Reads as "try primary; if absent, try fallback; if absent, use anonymous." All three are Optional<User>; the chain returns the first non-empty one. Different from orElseor keeps the result wrapped; orElse unwraps it with a plain T default.

Primitive specialisations

There are OptionalInt, OptionalLong, OptionalDouble for primitive results — what IntStream.max() returns, for example:

OptionalInt max = nums.stream().mapToInt(Integer::intValue).max();
int hi = max.orElse(0);

They have a smaller API — no map/flatMap/filter — because they sit on the boundary of the primitive world. Use them to read primitive-stream results; convert to Optional<Integer> if you need the full API.

Where Optional does not belong

The JDK's design intent is narrow: Optional is a return type for methods whose answer might not exist. It is not:

  • A field type. Don't write private Optional<String> middleName;. It's not Serializable, it costs an allocation per field, and a null field is shorter and clearer for "this entity has no middle name." The right move is a non-Optional field that may be null, with a getter that returns Optional.
  • A method parameter. Don't accept Optional<String> as an argument. Overload the method, or accept String and document that null means absent. Optional parameters require the caller to wrap, which is noise.
  • A collection element. List<Optional<T>> is almost always a list with null-able elements and extra wrapping. Use List<T> and filter the nulls out at the boundary, or use flatMap(Optional::stream) to drop the absents in a pipeline.
  • A way to avoid all null. Java still has null in every reference type; Optional is for the return-shape of code that produces values which might not exist. Plain reference types are fine for everything else.

The shorter rule: an Optional flowing out of a method is good design; an Optional flowing in is almost always wrong.

A worked example: every method, plus the rules-of-thumb in code

The program below builds a tiny user/address graph, walks every method on Optional against it, demonstrates orElse vs. orElseGet evaluation timing, the Optional.stream() bridge, and the or chain.

java— editable, runs on the server

What to take from the run:

  • The three constructors of, empty, ofNullable map to three crisp intents: definitely present, definitely absent, and legacy-adapter, present-if-non-null. Optional.of(null) throws — and that's the desired failure, not a bug to work around.
  • orElse evaluated its argument every time, even when the optional was present. orElseGet's supplier only ran when needed. Use orElse for cheap literals and orElseGet for anything that allocates, queries, or throws.
  • map and flatMap made the whole userById(...).flatMap(User::address).map(Address::city) chain read as a single pipeline — no null checks, no nested ifs, and any empty step short-circuits to Optional.empty() at the end.
  • flatMap(Optional::stream) turned a Stream<Optional<User>> into a Stream<User> with all the absents dropped in one go. That's the clean way to bridge a list of "might-fail" lookups into a stream of successes.
  • OptionalInt is what primitive-stream terminals like IntStream.findFirst return. It has its own small API (getAsInt, orElse, ifPresent) and exists so primitive pipelines never have to box.
  • The "wrong places" rule of thumb showed up implicitly: User.address was an Optional<Address> field — fine because the example wanted to demonstrate the API, but in production code the field would be a possibly-null Address with a Optional<Address> address() getter doing the wrapping.

What's next

Part 12 covered the functional vocabulary end to end: functional interfaces, lambdas, method references, the built-ins, the stream pipeline, every source, every intermediate, every terminal, collectors, parallel execution, and finally Optional as the type-level expression of absence. The next chapter, Java Predicate Interface, zooms back in on a single functional interface — Predicate<T> — and the combinator algebra (and, or, negate, isEqual, not) that lets you assemble predicates without ever writing the boolean glue by hand. From there the part continues with Function, Consumer/Supplier, and the binary-operator family — one interface per chapter, each with the same worked-example shape you've seen here.

Practice

Practice

You have `Optional<String> opt` and need a default when it's empty, where the default is an expensive call to `loadDefaultFromDb()`. Which is correct *and* avoids running the expensive call when `opt` is present?