Optional: Correct Use and Common Abuse
What Optional was designed for and what it was not, the orElse versus orElseGet trap that evaluates the fallback every time, chaining with map and flatMap, and why Optional fields are a mistake.
On this page
Optional arrived to make "this method might not return anything" visible in a signature. It works
well for exactly that, and most of the criticism it attracts comes from using it for other things.
Key Takeaways
- Designed as a return type for methods that may legitimately find nothing. Not a field, not a parameter, not a collection element.
orElse(x)always evaluatesx.orElseGet(() -> x)evaluates it only when empty.- Chain with
map,flatMapandfilterrather than unwrapping and re-checking. - Never return
nullfrom a method returningOptional— that is the worst of both designs. - An empty collection already means "nothing"; wrapping one in an
Optionaladds a state nobody needs.
What it is for
// Before: does this return null? The signature does not say. The javadoc might.
public Customer findByEmail(String email);
// After: the caller cannot ignore the empty case — it will not compile.
public Optional<Customer> findByEmail(String email);That is the entire value proposition. It moves a fact from documentation, where it is ignored, into the type system, where the compiler enforces it.
String displayName = customers.findByEmail(email)
.map(Customer::name) // Optional<String>
.filter(name -> !name.isBlank())
.orElse("Unknown customer");No null check appears anywhere, and there is no path through this code that produces a
NullPointerException.
orElse versus orElseGet
This is the most-asked Optional question, and it has a real cost behind it.
public Config load(String key) {
return cache.find(key)
.orElse(loadFromDatabase(key)); // ALWAYS runs the database call
}
public Config loadCorrectly(String key) {
return cache.find(key)
.orElseGet(() -> loadFromDatabase(key)); // only on a cache miss
}orElse takes a value. Java evaluates arguments before the call, so loadFromDatabase(key) runs
whether or not the cache had the entry — and on a hit, its result is computed and discarded. With a
95% hit rate you have built a cache that queries the database on every request.
| Method | Argument | Evaluated |
|---|---|---|
orElse(T) | A value | Always |
orElseGet(Supplier<T>) | A supplier | Only when empty |
orElseThrow() | — | Throws NoSuchElementException when empty |
orElseThrow(Supplier<X>) | An exception supplier | Only when empty |
The rule: use orElse for a constant or an already-computed value; orElseGet for anything that
does work.
Chaining
// map: the function returns a plain value
Optional<String> city = findCustomer(id).map(Customer::address).map(Address::city);
// flatMap: the function itself returns an Optional — avoids Optional<Optional<T>>
Optional<Manager> manager = findEmployee(id).flatMap(Employee::manager);
// ^ returns Optional<Manager>
// filter: keeps the value only if the predicate passes
Optional<Order> largeOrder = findOrder(ref).filter(o -> o.total().compareTo(LIMIT) > 0);
// or(): fall back to another Optional, lazily (Java 9)
Optional<Config> config = fromEnvironment(key).or(() -> fromFile(key)).or(() -> fromDefaults(key));
// ifPresentOrElse: two branches, no unwrapping (Java 9)
findCustomer(id).ifPresentOrElse(
this::sendWelcomeEmail,
() -> log.warn("no customer for {}", id));
// stream(): flatten a stream of Optionals (Java 9)
List<Customer> found = ids.stream().map(this::findCustomer).flatMap(Optional::stream).toList();map versus flatMap is the standard follow-up, and the answer is mechanical: if the function you
are applying already returns an Optional, use flatMap, otherwise you end up with
Optional<Optional<T>>.
The anti-patterns
// 1. isPresent + get — this is the null check Optional was meant to replace
if (opt.isPresent()) { use(opt.get()); }
opt.ifPresent(this::use); // better
// 2. Optional as a field: not Serializable, extra allocation per instance
public class Customer { private Optional<String> phone; } // use a nullable String
// 3. Optional as a parameter: three states for the caller instead of one
public void search(String term, Optional<String> region); // overload instead
// 4. Optional wrapping a collection: empty list already means "none"
public Optional<List<Order>> findOrders(String id); // return an empty List
// 5. Returning null from an Optional method — the worst of both worlds
public Optional<X> find() { return null; } // return Optional.empty()
// 6. Optional.of on something that might be null
return Optional.of(map.get(key)); // NPE — use ofNullableNumber two deserves the detail because it is the most argued about. Optional does not implement
Serializable, so an Optional field breaks Java serialisation outright and confuses JPA, Jackson
and most mapping frameworks. It also costs one extra object per field per instance. A nullable field
with an Optional-returning getter gives you the API benefit with none of the cost:
public class Customer {
private String phone; // nullable field
public Optional<String> phone() { // Optional at the API boundary
return Optional.ofNullable(phone);
}
}Number six is worth a habit: Optional.of throws if its argument is null, Optional.ofNullable
returns empty. Use of when null would be a bug you want to hear about, ofNullable when null is
expected.
Optional and Spring Data
Spring Data supports Optional return types directly, which is where most Java developers meet it:
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByEmail(String email);
}@Transactional(readOnly = true)
public CustomerView get(String email) {
return repository.findByEmail(email)
.map(CustomerView::from)
.orElseThrow(() -> new CustomerNotFoundException(email));
}Three lines, no null check, and a specific exception that a @ControllerAdvice can turn into a 404.
This is Optional doing exactly the job it was designed for, and it is a good example to volunteer
when asked where you have used it.
Performance, honestly
Every Optional is an object allocation, except Optional.empty() which is a shared singleton. On a
hot path returning millions of values per second that is measurable, which is why the JDK provides
OptionalInt, OptionalLong and OptionalDouble — and why Stream.findFirst returning an
Optional is a cost the JDK accepted deliberately.
In ordinary application code — a repository call, a configuration lookup, an HTTP handler — the
allocation is noise next to the work being done, and escape analysis often removes it entirely. Do not
avoid Optional for performance reasons without a profile that says to.
Optional does not replace validation
A recurring misuse is treating Optional as a general error-handling type. It carries exactly one
bit of information — present or absent — and no reason. A method that can fail for five distinct
reasons and returns Optional.empty() for all of them has thrown away everything the caller needed
to respond correctly.
The distinction to hold onto is that an empty Optional should mean "there is legitimately no value
here", not "something went wrong". A lookup that finds no customer is the first; a lookup that failed
because the database was unreachable is the second, and belongs as an exception. When a caller
genuinely needs a reason, the answer is a result type — a sealed interface with Found and
NotFound(reason) variants — rather than stretching Optional to carry information it has no room
for.
The same argument explains why Optional is a poor fit for a method that returns a collection.
Optional<List<Order>> has three states — absent, present-and-empty, present-with-items — where the
domain only has two, and every caller has to handle the extra one. Returning an empty list is both
simpler and impossible to misuse.
Interoperating with frameworks
Jackson serialises Optional correctly only with the jdk8 module registered, and by default an
empty Optional becomes null in JSON rather than being omitted. Spring Data supports Optional
return types on repository methods natively. Spring MVC accepts Optional on @RequestParam and
@PathVariable, which is one of the few places an Optional parameter is idiomatic, because the
framework constructs it rather than a caller.
JPA is the notable exception: an Optional entity field will not map, because Hibernate needs to
read and write the field directly. The nullable-field-with-Optional-getter pattern above is the
standard resolution, and it is worth naming in an interview because it shows you know where the
guidance stops applying.
What gets asked
Almost always: "what is the difference between orElse and orElseGet?" Then "why shouldn't
Optional be a field?" Then something about map versus flatMap. Having a real example of the
orElse trap — a cache that queried the database on every hit — makes the first answer memorable
rather than recited.
Frequently Asked Questions
What is the difference between orElse and orElseGet?
Why should Optional not be used as a field or a method parameter?
Is calling Optional.get() ever acceptable?
Related tutorials
- Parallel Streams and the Common ForkJoinPoolWhy every parallel stream in your JVM shares one pool, which sources split well, the N times Q rule for deciding, and why a blocking call inside a parallel stream can stall the whole application.
- Default & Static Methods in InterfacesWhy default methods were added, the three resolution rules when a class inherits conflicting defaults, calling a specific supertype with X.super.method(), and private interface methods.
- Collectors, groupingBy and Downstream CollectorsThe collector API in depth: multi-level groupingBy, downstream collectors, the toMap duplicate-key exception, the null-value trap, teeing and flatMapping, and writing a Collector by hand.
- The java.time APIChoosing between Instant, LocalDateTime and ZonedDateTime, Period versus Duration, what happens at a daylight-saving gap, and how to store timestamps so they survive a zone change.