Optional, used correctly
findFirst() and max() handed back an Optional, not a value. That is
deliberate: the stream might have been empty, and Optional is how a method says
"there may be nothing here" in a way the type system enforces.
It is also the most misused type in modern Java, because a great deal of code uses it as a null with extra steps. This lesson is as much about what not to do with it.
The problem it solves
Sub found = findByName("Nobody");
System.out.println(found.area());
That compiles. If findByName returns null when nothing matches, it throws
NullPointerException at runtime, and nothing in the signature warned you.
Optional<Sub> found = findByName("Nobody");
System.out.println(found.area());
That does not compile. The signature now states that the result may be absent,
and the compiler makes you deal with it. The value of Optional is entirely in
the signature — it turns a documented convention into a checked one.
Creating and inspecting
Optional.of("Priya") // Optional[Priya] — throws if you pass null
Optional.empty() // Optional.empty
Optional.ofNullable(maybeNull) // empty if null, otherwise present
Optional.of(null) throws NullPointerException
That throw is intentional: Optional.of is for values you know are present, so a
null there is a bug you want to hear about immediately.
Using one properly
The whole API, in the order you should reach for it:
| Method | Does |
|---|---|
map(f) |
Transform if present, stay empty otherwise |
flatMap(f) |
Same, when f itself returns an Optional |
filter(p) |
Keep the value only if it matches |
orElse(other) |
The value, or this fallback |
orElseGet(supplier) |
The value, or call this to make one |
orElseThrow() |
The value, or NoSuchElementException |
orElseThrow(supplier) |
The value, or your exception |
ifPresent(consumer) |
Do this if there is a value |
ifPresentOrElse(c, r) |
Do this, or that |
or(supplier) |
This optional, or another one |
stream() |
Zero or one element, for use in a pipeline |
isPresent() / isEmpty() |
Ask directly. Use sparingly |
get() |
The value, or an exception. Avoid |
The idiomatic shape is a chain that never asks whether the value is there:
findByName("Priya").map(Sub::area).map(String::toUpperCase).orElse("UNKNOWN");
findByName("Nobody").map(Sub::area).map(String::toUpperCase).orElse("UNKNOWN");
WAGHOLI
UNKNOWN
Each map is skipped when the optional is empty, so there is no place for a null
check. Compare with the version you are trying to avoid:
Optional<Sub> found = findByName("Priya");
if (found.isPresent()) {
return found.get().area().toUpperCase();
} else {
return "UNKNOWN";
}
That is the null check again, wearing a different hat. isPresent() followed
by get() is the anti-pattern — if you have written it, there is a map and
an orElse waiting to replace it.
The trap: orElse always evaluates its argument
This one costs real money in real systems.
Sub a = found.orElse(expensiveDefault());
Sub b = found.orElseGet(Opt::expensiveDefault);
orElse on a PRESENT optional called the default 1 time(s)
orElseGet on a PRESENT optional called the default 0 time(s)
The optional was present in both cases. orElse still called
expensiveDefault(), because its argument is an ordinary expression evaluated
before the method runs. orElseGet takes a Supplier and only calls it when
needed.
For a constant — orElse("unknown"), orElse(0) — this does not matter. For
anything that queries a database, reads a file, creates an object or logs, it
matters a great deal: you are doing the expensive fallback work on every call,
including the ones that did not need it.
orElse for constants, orElseGet for anything else. The same distinction
applies to orElseThrow(() -> new ...), which is why that one takes a supplier
too.
get() and why to avoid it
missing.get();
missing.orElseThrow();
missing.orElseThrow(() -> new IllegalArgumentException("no subscriber named Nobody"));
threw: No value present
orElseThrow: No value present
custom: no subscriber named Nobody
get() throws NoSuchElementException: No value present — a message that tells
whoever reads the log nothing at all. orElseThrow() does exactly the same thing
with a name that admits it might throw. And the supplier version lets you say
what was actually missing, which is the only one of the three worth writing.
If you are going to throw, throw something that names the problem.
Where Optional does not belong
This is the important half of the lesson, and it is the half most tutorials skip.
Not as a field. private Optional<String> middleName; adds an object per
instance, does not serialise properly with most JSON and JPA tooling, and is not
Serializable. Use a nullable field and return an Optional from the getter if
you like.
Not as a method parameter. void notify(Optional<String> phone) forces every
caller to wrap, and callers can still pass null for the optional itself — so
you have added ceremony and removed nothing. Use an overload, or accept null and
document it.
Not in a collection. List<Optional<Sub>> is almost always a sign that empty
entries should have been filtered out. Map<String, Optional<Sub>> is worse: a
missing key already means absent.
Never Optional.of(x) where x might be null. That throws. Use
ofNullable.
Not as a return type for a collection. An empty List already says "nothing
here". Optional<List<Sub>> gives callers two ways to mean the same thing, and
they will handle one of them.
The one place it belongs: a return type for a single value that may legitimately
be absent. findByName, max, firstMatching. That is what the standard
library uses it for, and following that convention is the whole benefit.
Bridging to and from null
Optional.ofNullable(map.get(key)); // null becomes empty
optional.orElse(null); // empty becomes null, at a boundary
The second is legitimate when handing a value to an older API that expects null.
Inside your own code, keep the Optional and chain.
Check your work
What does Optional actually buy you? A signature that says the result may be
absent, enforced by the compiler. Returning null documents the same thing only
by convention.
What is the difference between Optional.of and Optional.ofNullable?
of throws NullPointerException on null — use it when you know there is a
value. ofNullable turns null into empty.
Why is isPresent() then get() an anti-pattern? It is a null check with
extra syntax. A map and an orElse express the same thing without ever asking.
What is wrong with orElse(expensiveDefault())? The argument is evaluated
before the method runs, so the fallback executes even when the optional is
present. Use orElseGet(...) for anything but a constant.
What does get() throw, and what should you use instead?
NoSuchElementException: No value present, with no useful detail.
orElseThrow(() -> new IllegalArgumentException("no subscriber named " + name))
names the problem.
Name three places Optional does not belong. Fields, method parameters,
inside collections, and as a return type where an empty collection already says
"nothing".
Where does it belong? As the return type of a method producing a single value that may legitimately be absent.
Practice 3, the evaluation counter. With a present optional,
orElse(makeDefault()) calls makeDefault once and orElseGet(Opt::makeDefault)
calls it zero times. With an empty optional both call it once. The lesson is that
orElse costs you the fallback on the happy path, which is the path that runs
most often.
Practice 5, the refactor. The isPresent/get version becomes
findByName(name).map(Sub::area).map(String::toUpperCase).orElse("UNKNOWN").
If your version still contains an if, look for the map that replaces it.
Practice
-
Write a finder.
Optional<Sub> findByName(String)usingstream().filter(...).findFirst(). Call it with a name that exists and one that does not, and print both results directly. -
Chain without asking. Get the uppercase area for a name, defaulting to
"UNKNOWN", using onlymapandorElse. Noif, noisPresent. -
Count the evaluations. Write a
makeDefault()that increments a counter and returns a value. CallorElse(makeDefault())andorElseGet(this::makeDefault)on a present optional and print the counter after each. -
Throw usefully. Use
orElseThrow()and thenorElseThrow(() -> new IllegalArgumentException(...))naming the missing subscriber. Compare the two messages as they would appear in a log. -
Refactor the anti-pattern. Write the
isPresent/getversion of exercise 2 first, then convert it. Keep both and read them side by side. -
Harder — a lookup chain. Given a pincode, find the area; given the area, find the delivery round; given the round, find the driver's phone number. Each step may fail. Write it with
flatMapso the whole chain is one expression returningOptional<String>, and make the failure message at the end say which step failed — which will require you to stop usingOptionalat some point and use aResulttype like the one from module 4 instead. Working out where that line falls is the exercise.
Next: grouping and partitioning, which is where streams start replacing whole reports.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship