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 emptyThe 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 isPresentYou'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 emptyopt.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 exceptionorElse(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()— throwNoSuchElementExceptionif absent. The Java 10+ no-arg form is the modern equivalent ofopt.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 orElse — or 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 notSerializable, it costs an allocation per field, and anullfield is shorter and clearer for "this entity has no middle name." The right move is a non-Optional field that may benull, with a getter that returnsOptional. - A method parameter. Don't accept
Optional<String>as an argument. Overload the method, or acceptStringand document thatnullmeans absent. Optional parameters require the caller to wrap, which is noise. - A collection element.
List<Optional<T>>is almost always a list withnull-able elements and extra wrapping. UseList<T>and filter the nulls out at the boundary, or useflatMap(Optional::stream)to drop the absents in a pipeline. - A way to avoid all
null. Java still hasnullin every reference type;Optionalis 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.
What to take from the run:
- The three constructors
of,empty,ofNullablemap 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. orElseevaluated its argument every time, even when the optional was present.orElseGet's supplier only ran when needed. UseorElsefor cheap literals andorElseGetfor anything that allocates, queries, or throws.mapandflatMapmade the wholeuserById(...).flatMap(User::address).map(Address::city)chain read as a single pipeline — nonullchecks, no nested ifs, and any empty step short-circuits toOptional.empty()at the end.flatMap(Optional::stream)turned aStream<Optional<User>>into aStream<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.OptionalIntis what primitive-stream terminals likeIntStream.findFirstreturn. 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.addresswas anOptional<Address>field — fine because the example wanted to demonstrate the API, but in production code the field would be a possibly-nullAddresswith aOptional<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
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?