RizTech Academy logo
RizTech Academy
Streams and Functional JavaLesson 3 of 525 min

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

  1. Write a finder. Optional<Sub> findByName(String) using stream().filter(...).findFirst(). Call it with a name that exists and one that does not, and print both results directly.

  2. Chain without asking. Get the uppercase area for a name, defaulting to "UNKNOWN", using only map and orElse. No if, no isPresent.

  3. Count the evaluations. Write a makeDefault() that increments a counter and returns a value. Call orElse(makeDefault()) and orElseGet(this::makeDefault) on a present optional and print the counter after each.

  4. Throw usefully. Use orElseThrow() and then orElseThrow(() -> new IllegalArgumentException(...)) naming the missing subscriber. Compare the two messages as they would appear in a log.

  5. Refactor the anti-pattern. Write the isPresent/get version of exercise 2 first, then convert it. Keep both and read them side by side.

  6. 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 flatMap so the whole chain is one expression returning Optional<String>, and make the failure message at the end say which step failed — which will require you to stop using Optional at some point and use a Result type 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