Topic 10.7
Optional
In one line
Optional<T> is a box that holds either one value or nothing, used as a method's return type to say "there may be no result". Instead of returning null and hoping the caller checks, you return an Optional and the caller must decide what happens when it's empty, with orElse, orElseThrow, map and friends.
Think of it like this
A parcel with a label that says "may be empty". Before using what's inside, you naturally check, or you decide in advance ("if it's empty, use the spare charger"). Returning null is like handing someone nothing at all and letting them discover it when they try to plug it in. Optional is the labelled parcel.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Optional
- A container object that holds either one non-null value or nothing, used to show that a result may be missing.
- Empty optional
- An
Optionalwith no value inside, writtenOptional.empty(). - Default value
- The value to use when the real one is missing, like
"guest"when there's no user name. - Eager evaluation
- Computing something right away, even if it ends up not being used.
- Lazy evaluation
- Computing something only at the moment it's actually needed.
- NullPointerException
- The exception thrown when code uses a
nullreference as if it pointed to an object, for example calling a method on it. - Value-based class
- A class like
Optionalwhose objects should be treated only as their values: don't compare them with==or use them as locks.
Step by step
01The problem with returning null
String findEmail(String user) might return null when the user doesn't exist, but nothing in the signature says so. The caller writes findEmail(u).toLowerCase() and gets a NullPointerException, often far away from where the null came from. Tony Hoare, who invented null references in 1965, later called them his "billion-dollar mistake".
Optional<String> findEmail(String user) puts the possibility into the type. The caller can't call toLowerCase() on an Optional; they have to unwrap it and decide what an empty result means.
// before: the caller has to remember to check
String email = findEmailOrNull(user);
if (email != null) {
send(email);
}
// after: the type tells the caller, and the API makes both cases explicit
findEmail(user).ifPresentOrElse(
e -> send(e),
() -> log("no email for " + user));02Creating an Optional
Optional.of(value) is for values you know aren't null; if one is, it fails immediately with NullPointerException, which is what you want. Optional.ofNullable(value) is the bridge from old APIs that return null, like Map.get. Optional.empty() is the "nothing" value; there's only one empty Optional object in the JVM.
03Getting the value out
Pick the method that says what an empty result means in your code: a sensible default (orElse), a computed default (orElseGet), or an error (orElseThrow). Each of these is one call; you never need isPresent() followed by get().
if (opt.isPresent()) { use(opt.get()); } is just a null check in disguise, with more objects. Write opt.ifPresent(this::use) or opt.map(...).orElse(...) instead.
String a = opt.orElse("guest"); // default value
String b = opt.orElseGet(() -> loadDefaultName()); // computed only if empty
String c = opt.orElseThrow(); // Java 10: NoSuchElementException if empty
String d = opt.orElseThrow(() -> new UserNotFound(id)); // your own exception
opt.ifPresent(name -> greet(name)); // only when present
opt.ifPresentOrElse(this::greet, this::greetStranger); // Java 9: both cases04orElse is eager, orElseGet is lazy
opt.orElse(expensiveDefault()) is an ordinary method call: Java evaluates the argument expensiveDefault() first, then passes the result to orElse, which ignores it if a value is present. So the expensive work happens every time.
opt.orElseGet(() -> expensiveDefault()) passes a Supplier (Topic 10.2). orElseGet calls it only when the Optional is empty. Use orElse for constants and existing variables, orElseGet for anything that computes, allocates or has side effects.
05map, flatMap and filter replace nested null checks
Reading order.customer().address().city() safely used to take three nested if (x != null) blocks. With Optional, each step is a map: if any step produces null, the result becomes empty and the remaining steps are skipped.
Use flatMap when the step itself returns an Optional (like Customer::addressOpt); map would give you Optional<Optional<Address>>. filter(p) keeps the value only if it matches, which is handy for validation: pin.filter(p -> p.length() == 6).
06Optional and streams
Stream terminals that may find nothing return an Optional: findFirst, findAny, min, max, reduce(op). Primitive streams return OptionalInt, OptionalLong or OptionalDouble.
Going the other way, opt.stream() (Java 9) is a stream of zero or one element. list.stream().map(this::lookup).flatMap(Optional::stream) turns a stream of optional results into a stream of only the found ones. first.or(() -> second) (Java 9) tries a fallback source, and calls it only when needed.
07Where Optional doesn't belong
Fields: Optional isn't Serializable, costs an extra object per field, and frameworks (JPA, many JSON mappers) don't expect it. Keep the field nullable and return Optional.ofNullable(field) from the getter.
Parameters: void send(Optional<String> cc) forces every caller to wrap, and they can still pass null. Use overloads or a nullable parameter with a clear contract.
Collections: return an empty List, not Optional<List<T>> or a list of Optionals. And never return null from a method declared to return Optional: callers will rightly not check, and the NPE lands on them.
Try it yourself
- 1
Prove orElse is eager
In the orElse example, replace
orElseGet(Main::expensiveDefault)withorElse(expensiveDefault())in the second block. Predict the new value ofcallsat the end, then run. - 2
map versus flatMap
In the nested-null example, change
.flatMap(Customer::addressOpt)to.map(Customer::addressOpt)insidecity. Read the compiler error: what type does the next.map(Address::city)receive? - 3
Chain three sources
Add a
fromDefaults(String key)that returnsOptional.of("default:" + key), and chain it:fromCache(key).or(() -> fromDatabase(key)).or(() -> fromDefaults(key)). Predict all three lines before you run it.
Code & diagrams
Expected output
Optional[asha@mail.in] / Optional.empty
isPresent: true, isEmpty: true
orElse: no email
orElseGet: generated@mail.in
orElseThrow: asha@mail.in
send to asha@mail.in
nothing to send for zoya
caught: unknown user: zoya
Optional.of(null) throws NullPointerExceptionThe first call computed a default and threw it away. With a database lookup instead of a print, that's a real cost on every call.
Expected output
orElse on a present value:
(computing default...)
result cached
orElseGet on a present value:
result cached
orElseGet on an empty value:
(computing default...)
result default
default computed 2 timesExpected output
PUNE | PUNE
UNKNOWN | UNKNOWN
UNKNOWN | UNKNOWN
UNKNOWN | UNKNOWN
pin starts with 41: true
pin starts with 56: false
map instead of flatMap: Optional[Optional[Address[city=Pune, pin=411001]]]or() takes a Supplier, so the database is only asked when the cache misses. Key a never reaches it.
Expected output
a -> cache:a
db lookup for b
b -> db:b
db lookup for c
c -> not found
cache hits: [cache:a]
OptionalInt[10] -> 10
max of nothing: OptionalInt.emptyBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call get() on an empty Optional
Write Optional<String> nick = Optional.empty(); System.out.println(nick.get());.
Break #2
Wrap a null with Optional.of
Write String middleName = null; Optional<String> m = Optional.of(middleName);.
Break #3
Return null from a method that returns Optional
Write static Optional<String> findNickname(String user) { return null; } and call findNickname("asha").orElse("no nickname").
Myth vs fact
Myth
Optional eliminates NullPointerExceptions.
Fact
It makes absence visible in return types, but Optional.of(null), get() on empty, and a method returning a null Optional all still fail. It's a design tool, not a guarantee.
Myth
You should use Optional everywhere instead of null.
Fact
Its designers intended it for return types where "no result" is a normal outcome. Fields, parameters, collections and hot inner loops are better served by plain references, empty collections and clear contracts.
Myth
orElse only runs the default when the Optional is empty.
Fact
The argument to orElse is evaluated before the call, every time. Only orElseGet's supplier is lazy.
Myth
isPresent() + get() is the normal way to use Optional.
Fact
It's the least useful way: a null check with extra steps. map, orElse, orElseThrow and ifPresent express the intent in one call.
When it breaks
orElse(createDefaultAccount()) creates an account on every login
What you see
A login path does repo.find(id).orElse(createDefaultAccount(id)). Because orElse evaluates its argument eagerly, a default account row is inserted (or an expensive call is made) on every login, even for existing users. Symptoms: duplicate rows, unique-constraint errors, or unexplained load on a downstream service.
Fix & prevent
Use orElseGet(() -> createDefaultAccount(id)) so the default is created only when the lookup is empty. Add a test that counts calls to the default factory for an existing user.
A repository method declared Optional returns null on a cache miss
What you see
Callers write repo.find(id).map(...) without null checks, as they should. A new code path returns null instead of Optional.empty(), and requests fail with NullPointerException: Cannot invoke "java.util.Optional.map(...)" because the return value of ... is null.
Fix & prevent
Return Optional.empty() on every miss, add a static-analysis rule (for example Error Prone's or Sonar's "null returned from Optional method") and a unit test for the miss path.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Optionalis a value-based class:==,synchronized, and identity hash codes on it are unreliable by contract.Optional.empty()returns a shared singleton today, but code must not depend on that. Project Valhalla plans to turn such classes into identity-free value classes, where anOptionalcould be flattened and cost nothing extra. - ▸
Each
Optionalis a heap object (16 bytes on a typical 64-bit JVM with compressed pointers). Escape analysis often removes short-lived ones in a chain likeofNullable(x).map(...).orElse(...)after inlining, but anOptionalstored in a field or a collection always costs its allocation. That's one reason not to use them as fields. - ▸
Optionaldeliberately isn'tSerializable, which keeps it out of serialized object graphs and RPC signatures. Jackson needs thejdk8datatype module to (de)serializeOptionalproperties, which is another hint that it's meant for method returns, not data models. - ▸
API history: Java 9 added
ifPresentOrElse,orandstream; Java 10 added the no-argumentorElseThrow()precisely so code could stop using the misleadingly namedget(); Java 11 addedisEmpty(). IDE inspections flagget()without a check, andOptional<Optional<T>>usually means amapshould have beenflatMap.
Remember this
- 1
Optional<T>(packagejava.util) contains at most one value. You create one withOptional.of(value)(the value must not benull, or it throwsNullPointerException),Optional.ofNullable(value)(empty ifnull) orOptional.empty(). It prints asOptional[value]orOptional.empty. - 2
Getting the value out safely:
orElse(default)returns the value or a default;orElseGet(supplier)computes the default only if needed;orElseThrow()(Java 10) throwsNoSuchElementExceptionif empty, andorElseThrow(() -> new MyException(...))throws your exception.get()does the same asorElseThrow()but its name hides the danger, so avoid it.isPresent()andisEmpty()(Java 11) just test. - 3
Acting on it:
ifPresent(consumer)runs code only when there's a value, andifPresentOrElse(consumer, runnable)(Java 9) handles both cases. Transforming it:map(f)appliesfto the value (and returns empty iffreturnsnull),flatMap(f)is for functions that already return anOptional,filter(p)empties it when the predicate fails, andor(supplier)(Java 9) supplies anotherOptionalwhen this one is empty. - 4
orElse(x)always evaluatesxfirst, because Java evaluates method arguments before the call, even when the value is present. If computing the default is costly or has side effects (a database call, creating an object, logging), useorElseGet(() -> ...). - 5
Optional was designed for return types. Don't use it for fields (it isn't
Serializableand adds an object per field), for method parameters (callers then passnullOptionals or wrap everything), or inside collections (return an empty list instead ofOptional<List>). And a method that returnsOptionalmust **never returnnull**. - 6
Streams use it everywhere a result might not exist:
findFirst,findAny,min,maxand identity-lessreduce(Topic 10.5).Optional.stream()(Java 9) turns it into a stream of zero or one element, soflatMap(Optional::stream)keeps only the present values. For primitives there areOptionalInt,OptionalLongandOptionalDouble, withgetAsInt()and friends.
Explain it without notes
Why was Optional added, and what is it meant to be used for?
What's the difference between orElse and orElseGet?
When do you use map and when flatMap on an Optional?
Where should you not use Optional, and why?
Practice
Write static Optional<Integer> parseAge(String s) that returns empty for non-numbers and for ages outside 0..150. Print the result for "42", "abc" and "200", using orElse(-1).
Given Map<String, String> managers (employee to manager), write a method that returns the manager's manager as Optional<String>, using ofNullable and map. Test it on a three-level chain and on someone at the top.
From List.of("7", "x", "12", "", "5"), use your parseAge and flatMap(Optional::stream) to print the sum of the valid ages.
Trade-offs
- ↔
Returning
Optionaldocuments absence in the type and leads to fluent, null-free call sites, at the cost of an extra object and some verbosity. For hot paths or internal helpers, a documented nullable return or a sentinel can be acceptable. - ↔
orElseThrowturns absence into an exception early and clearly;orElsekeeps going with a default. Throwing is right when absence is a bug or a client error; a default is right when absence is normal. - ↔
Long
map/flatMapchains read well for navigation through optional data, but debugging which step went empty is harder than with explicitifstatements. Keep chains short, and name intermediate results when the logic matters.
Done when you can
Done when you can create Optionals with
of,ofNullableandempty, and know which throws onnull.Done when you never use
isPresent()followed byget(), and choose betweenorElse,orElseGetandorElseThrow.Done when you can explain why
orElseis eager and demonstrate it.Done when you can replace nested null checks with
map,flatMapandfilter.Done when you can list where Optional shouldn't be used and why.