Java Optional: The Methods Worth Knowing, and the One That Fools Everyone
Optional is a return type that says a value may be absent, and the compiler makes you deal with it. Use map and filter to transform without unwrapping, then orElse, orElseGet or orElseThrow to leave. The trap is that orElse evaluates its fallback even when a value is present — measured, it ran an expensive method it did not need — while orElseGet takes a supplier and does not.
Most code that uses Optional uses two of its methods: isPresent() and get(). That works, and it is a null check with extra steps — the branch you were trying to remove, now wearing a wrapper.
This article covers the methods worth using, with output from a real run, and then one measurement that explains a bug people ship without noticing: orElse runs its fallback even when there is nothing to fall back to.
What Optional is for, and what it is not
It is a return type. A method that returns Optional<Order> is saying there may not be an order, in a way the compiler makes the caller acknowledge. Compare that with returning null, where the same fact is documented in a comment if you are lucky.
What it is not is null safety. Optional is an ordinary object, and an Optional reference can itself be null. A method that returns null instead of Optional.empty() has defeated the whole mechanism and the compiler will not stop it. The guarantee is a convention that the type system helps you keep.
The examples below use one list of five Order records — bob has the largest at 310 — and biggest is the Optional<Order> that comes out of max().
The methods you actually need
Transform without unwrapping, with map and filter:
Optional<Order> biggest = ORDERS.stream().max(comparingInt(Order::amount));
biggest.map(Order::customer).orElse("none"); // bob
biggest.filter(o -> o.amount() > 1000).map(Order::customer).orElse("none"); // none
The second line is the point of the whole class. The filter fails, so the Optional becomes empty, so the map does nothing, so the orElse supplies the fallback — and there is no branch anywhere. Absence propagates through the chain instead of being checked at every step.
Leave the Optional, three ways:
biggest.orElse(FALLBACK_ORDER); // a value
biggest.orElseGet(() -> buildFallback()); // a supplier, called only if needed
biggest.orElseThrow(); // NoSuchElementException if empty
Do something instead of getting something:
biggest.ifPresent(o -> log.info("largest: {} {}", o.customer(), o.amount()));
biggest.ifPresentOrElse(this::record, this::recordNothingFound);
And three that come up later:
biggest.flatMap(o -> Optional.ofNullable(o.region())); // Optional[north]
biggest.stream().map(Order::customer).toList(); // [bob]
Optional.<String>empty().or(() -> Optional.of("fallback")); // Optional[fallback]
flatMap is for when your mapper itself returns an Optional — map would give you Optional<Optional<String>>. stream() turns an Optional into a stream of zero or one elements, which is how you flatten a collection of Optionals. And or supplies a whole alternative Optional rather than a value.
isPresent() then get() is the pattern to unlearn
String name;
if (biggest.isPresent()) {
name = biggest.get().customer();
} else {
name = "none";
}
String name = biggest.map(Order::customer).orElse("none");
Same logic. The first version has a branch, a mutable local, and a get() whose safety depends on the if above it staying correct while the code around it is edited. The second has none of those, and reads as a statement about the value rather than about the container holding it.
Tip
A decent rule: if
get()appears in your code at all, something above it is doing a check thatmap,filterororElseThrowwould have done for you.orElseThrow()is the honest version ofget()— same behaviour, and the name says what it does.
orElse and orElseGet are not stylistic alternatives
This is the part worth the article. Give each one a fallback that announces itself when it runs, on an Optional that holds a value:
Optional<String> present = Optional.of("real");
present.orElse(expensive("orElse"));
present.orElseGet(() -> expensive("orElseGet"));
static String expensive(String who) {
System.out.println("!! expensive(" + who + ") actually ran");
return "fallback";
}
What it printed:
!! expensive(orElse) actually ran
orElse -> real
orElseGet -> real
Both returned "real", correctly. But orElse ran the fallback it did not need, and orElseGet did not.
And the reason is not a decision Optional made. orElse takes a value, so expensive("orElse") is an ordinary method call in an ordinary argument position, and Java evaluates arguments before the call happens. By the time orElse runs, the work is already done; there is nothing it could have skipped. orElseGet takes a Supplier, so what gets passed is a function that has not been called, and it is only called when the value is absent.
Which makes the rule simple:
orElsefor a constant or something already in hand —orElse(""),orElse(EMPTY_LIST),orElse(cachedDefault)orElseGetfor anything that does work — a query, a network call, an object allocation, anything with a side effect
Common mistake
orElse(repository.findDefault())on a hot path. It queries the database on every call, including the overwhelming majority where the Optional had a value and the result was thrown away. Nothing fails, nothing logs, and the query count is double what anyone expects.
Three hard edges
Optional.of(null); // NullPointerException
Optional.ofNullable(null); // Optional.empty
Optional.empty().get(); // NoSuchElementException: No value present
of is an assertion — there is a value here — and it enforces it. ofNullable is the one for a value that may legitimately be absent. Passing a possibly-null variable to of is the most common way to turn a null into an exception one layer further from its cause.
And get() on empty throws with exactly the message above — useful to recognise, because it means somebody checked nothing.
Optional is not Serializable
new ObjectOutputStream(out).writeObject(Optional.of("x"));
NotSerializableException: java.util.Optional
It does not implement Serializable, and that is deliberate rather than an oversight — it is a design signal about where the class belongs.
So it is a poor field type: anything that serialises your objects will refuse, and a field that may be absent is better expressed as a nullable field with a getter returning Optional. It is also a poor parameter type, for a different reason — it forces every caller to wrap an argument in order to say nothing, when an overload or a nullable parameter says it more cheaply.
Return type. That is the position it was designed for, and the only one where it pays.
Where you will meet it
Most people's first Optional comes back from a lookup that may find nothing — a repository findById, where the row may not exist. That is the case it was made for, and the database either has the row or it does not.
The useful thing to do with it at the edge of an application is to turn absence into the right response rather than into a null:
@GetMapping("/{id}")
public Order byId(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "No order " + id));
}
One expression: found means return it, absent means 404. That is the same shape as any other REST handler, with the absence handled by the type rather than by an if.
You need a JDK to run any of the examples above — installing Java 21 covers that. And the habit worth forming is the short version of this whole article: return it, chain it, and do not store it.
Frequently asked questions
- What is the difference between orElse and orElseGet?
- orElse takes a value and orElseGet takes a supplier, which means orElse's argument is evaluated before the call whether it is needed or not. Measured on an Optional that held a value, orElse still ran the fallback method and orElseGet did not. If the fallback is a constant it makes no difference; if it queries a database it is a bug.
- Why does Optional.of(null) throw?
- Because of() is for values you know are present, and it enforces that. Use ofNullable() for a value that may be null — it returns Optional.empty rather than throwing. The two exist so that "I expect a value here" and "there may be nothing here" are different statements in code.
- Should I use Optional as a field or a method parameter?
- No. It is not serializable — writing one to an ObjectOutputStream throws NotSerializableException — which rules out a lot of frameworks, and as a parameter it forces callers to wrap arguments to say nothing. Use it as a return type, which is what it was designed for.
- Is isPresent() followed by get() wrong?
- Not wrong, just pointless. It is a null check with more syntax and the same branch to get wrong. map().orElse() expresses the same logic as one expression with nothing to forget, and reads as a statement about the value rather than about the container.
- Does Optional protect against NullPointerException?
- Only where you use it as a return type and callers honour it. An Optional reference can itself be null — Optional is an object like any other — so a field or a method that returns null instead of Optional.empty defeats the whole point. The guarantee is a convention the compiler helps you keep, not a language feature.