Command Palette

Search for a command to run...

PHASE 10Intermediate Java 8+ ~31 min· topic 7 of 8

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 Optional with no value inside, written Optional.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 null reference as if it pointed to an object, for example calling a method on it.
Value-based class
A class like Optional whose 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.

Main.javawhole filejava
// 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.

Creating an Optionaldiagram
Rendering diagram…

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.

Main.javawhole filejava
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 cases

04orElse 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).

map, flatMap and filter replace nested null checksdiagram
Rendering diagram…

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.

terminal
$ java Main.java
── expected output ──
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.orElse(Object)" because the return value of "Main.findNickname(String)" is null
at Main.main(Main.java:9)

Try it yourself

  1. 1

    Prove orElse is eager

    In the orElse example, replace orElseGet(Main::expensiveDefault) with orElse(expensiveDefault()) in the second block. Predict the new value of calls at the end, then run.

  2. 2

    map versus flatMap

    In the nested-null example, change .flatMap(Customer::addressOpt) to .map(Customer::addressOpt) inside city. Read the compiler error: what type does the next .map(Address::city) receive?

  3. 3

    Chain three sources

    Add a fromDefaults(String key) that returns Optional.of("default:" + key), and chain it: fromCache(key).or(() -> fromDatabase(key)).or(() -> fromDefaults(key)). Predict all three lines before you run it.

Code & diagrams

Creating an Optional and getting the value out Java 11+ New tab
Sign in to run this example in your browser.

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 NullPointerException
orElse runs its argument even when it isn't needed Java 8+ New tab

The 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.

Sign in to run this example in your browser.

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 times
map, flatMap and filter instead of nested null checks Java 16+ New tab
Sign in to run this example in your browser.

Expected 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(), Optional::stream and OptionalInt Java 16+ New tab

or() takes a Supplier, so the database is only asked when the cache misses. Key a never reaches it.

Sign in to run this example in your browser.

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.empty

Break 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());.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.get(Optional.java:143)
at Main.main(Main.java:6)

Break #2

Wrap a null with Optional.of

Write String middleName = null; Optional<String> m = Optional.of(middleName);.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at java.base/java.util.Optional.of(Optional.java:113)
at Main.main(Main.java:6)

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").

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.orElse(Object)" because the return value of "Main.findNickname(String)" is null
at Main.main(Main.java:9)

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.

  • ▸

    Optional is 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 an Optional could be flattened and cost nothing extra.

  • ▸

    Each Optional is a heap object (16 bytes on a typical 64-bit JVM with compressed pointers). Escape analysis often removes short-lived ones in a chain like ofNullable(x).map(...).orElse(...) after inlining, but an Optional stored in a field or a collection always costs its allocation. That's one reason not to use them as fields.

  • ▸

    Optional deliberately isn't Serializable, which keeps it out of serialized object graphs and RPC signatures. Jackson needs the jdk8 datatype module to (de)serialize Optional properties, which is another hint that it's meant for method returns, not data models.

  • ▸

    API history: Java 9 added ifPresentOrElse, or and stream; Java 10 added the no-argument orElseThrow() precisely so code could stop using the misleadingly named get(); Java 11 added isEmpty(). IDE inspections flag get() without a check, and Optional<Optional<T>> usually means a map should have been flatMap.

Remember this

  1. 1

    Optional<T> (package java.util) contains at most one value. You create one with Optional.of(value) (the value must not be null, or it throws NullPointerException), Optional.ofNullable(value) (empty if null) or Optional.empty(). It prints as Optional[value] or Optional.empty.

  2. 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) throws NoSuchElementException if empty, and orElseThrow(() -> new MyException(...)) throws your exception. get() does the same as orElseThrow() but its name hides the danger, so avoid it. isPresent() and isEmpty() (Java 11) just test.

  3. 3

    Acting on it: ifPresent(consumer) runs code only when there's a value, and ifPresentOrElse(consumer, runnable) (Java 9) handles both cases. Transforming it: map(f) applies f to the value (and returns empty if f returns null), flatMap(f) is for functions that already return an Optional, filter(p) empties it when the predicate fails, and or(supplier) (Java 9) supplies another Optional when this one is empty.

  4. 4

    orElse(x) always evaluates x first, 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), use orElseGet(() -> ...).

  5. 5

    Optional was designed for return types. Don't use it for fields (it isn't Serializable and adds an object per field), for method parameters (callers then pass null Optionals or wrap everything), or inside collections (return an empty list instead of Optional<List>). And a method that returns Optional must **never return null**.

  6. 6

    Streams use it everywhere a result might not exist: findFirst, findAny, min, max and identity-less reduce (Topic 10.5). Optional.stream() (Java 9) turns it into a stream of zero or one element, so flatMap(Optional::stream) keeps only the present values. For primitives there are OptionalInt, OptionalLong and OptionalDouble, with getAsInt() and friends.

Explain it without notes

01

Why was Optional added, and what is it meant to be used for?

02

What's the difference between orElse and orElseGet?

03

When do you use map and when flatMap on an Optional?

04

Where should you not use Optional, and why?

Practice

01

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).

02

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.

03

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 Optional documents 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.

  • ↔

    orElseThrow turns absence into an exception early and clearly; orElse keeps 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/flatMap chains read well for navigation through optional data, but debugging which step went empty is harder than with explicit if statements. Keep chains short, and name intermediate results when the logic matters.

Done when you can

  • Done when you can create Optionals with of, ofNullable and empty, and know which throws on null.

  • Done when you never use isPresent() followed by get(), and choose between orElse, orElseGet and orElseThrow.

  • Done when you can explain why orElse is eager and demonstrate it.

  • Done when you can replace nested null checks with map, flatMap and filter.

  • Done when you can list where Optional shouldn't be used and why.