Lesson 23 • Advanced
Working with Optionals
Stop fighting NullPointerException. Learn to model "a value that might not be there" with Optional, and write safe chains that never crash on missing data.
What You'll Learn in This Lesson
- ✓Why Optional exists and how it stops NullPointerException
- ✓Create boxes with of(), empty(), and ofNullable()
- ✓Check contents with isPresent() and isEmpty() — and why get() is risky
- ✓Transform values safely with map(), flatMap(), and filter()
- ✓Supply defaults with orElse(), orElseGet(), and orElseThrow()
- ✓React with ifPresent() / ifPresentOrElse() and avoid the anti-patterns
Before You Start
You'll get the most from this lesson if you already know Generics (Optional is a generic container, Optional<T>) and Lambda Expressions (its best methods like map() and orElseGet() take lambdas). If those feel shaky, peek back first.
🎁 A Real-World Analogy: The Gift Box
Think of an Optional as a small gift box you hand someone. The box itself is always real — but it might be empty inside.
Optional.of(gift)— a box with a gift inside, guaranteed.Optional.empty()— an honest, deliberately empty box.Optional.ofNullable(x)— a box that has a gift only ifxisn't null.isPresent()— shake the box to check if there's something inside.orElse("backup")— "if it's empty, here's a spare gift instead."map()— rewrap the gift differently without ever opening the box.
A bare null is like handing someone nothing at all and not telling them — they reach out, grab air, and fall over (that's your NullPointerException). The box forces them to look before they grab.
1️⃣ Why Optional Exists (the null problem)
The inventor of the null reference, Tony Hoare, called it his "billion-dollar mistake." The trouble is that a method returning String might secretly return null, and nothing in the type warns you. Call a method on that null and your program crashes with a NullPointerException (NPE) — the single most common runtime error in Java.
Optional<T> fixes this by making absence visible in the type. A method that returns Optional<String> is openly saying "you might get nothing — handle that." The compiler nudges the caller; the bug surfaces while you read the code, not at 3am in production.
Three ways to put a value in the box:
Optional.of(x)— use whenxis never null. (If it is null, this throws immediately — a feature, not a bug.)Optional.empty()— an empty box, on purpose.Optional.ofNullable(x)— use whenxmight be null; you get an empty box if it is.
To peek without unwrapping, use isPresent() (true when there's a value) or its mirror isEmpty() (Java 11+).
import java.util.Optional;
public class Main {
public static void main(String[] args) {
// THE PROBLEM: a method that returns null when nothing is found.
// The caller has no warning that null is even possible.
String found = findUser(99); // returns null
// System.out.println(found.length()); // 💥 NullPointerException!
// THE FIX: return an Optional<String> instead of a bare String.
// The TYPE now shouts "this might be empty — handle it".
Optional<String> maybeUser = findUserSafely(99);
System.out.println("Was a user found? " + maybeUser.isPresent()); // false
// 3 ways to BUILD an Optional:
Optional<String> a = Optional.of("Alice"); // value is NEVER null
Optional<String> b = Optional.empty(); // deliberately nothing
Optional<String> c = Optional.ofNullable(null); // value MIGHT be null
System.out.println("of(\"Alice\") -> isPresent? " + a.isPresent()); // true
System.out.println("empty() -> isPresent? " + b.isPresent()); // false
System.out.println("ofNullable(null)-> isPresent? " + c.isPresent()); // false
System.out.println("ofNullable(null)-> isEmpty? " + c.isEmpty()); // true
}
static String findUser(int id) {
return null; // the old, dangerous style
}
static Optional<String> findUserSafely(int id) {
return Optional.empty(); // honest: "nothing here"
}
}Was a user found? false
of("Alice") -> isPresent? true
empty() -> isPresent? false
ofNullable(null)-> isPresent? false
ofNullable(null)-> isEmpty? true2️⃣ Getting the Value Out — Safely
Once you have an Optional, you eventually need the value. There's a tempting-but-dangerous way and several safe ways.
⚠️ Risky: get()
opt.get() hands you the value but throws NoSuchElementException on an empty box — the very crash you were avoiding.
✅ Safe: orElse / orElseGet / orElseThrow
These always handle the empty case — a fallback value, a lazily-built default, or a clear exception of your choice.
orElse(buildDefault()) always calls buildDefault(), even when the Optional has a value — wasteful if it's expensive. orElseGet(() -> buildDefault()) only runs the lambda when the box is empty. The worked example below proves it.import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> name = Optional.of("Alice");
Optional<String> empty = Optional.empty();
// get() — gives you the value, but is RISKY: it throws on an empty box.
System.out.println("get() on present: " + name.get()); // Alice
// empty.get(); // 💥 NoSuchElementException: No value present
// orElse(default) — the default is ALWAYS built, even when not used.
System.out.println("orElse: " + empty.orElse("Guest")); // Guest
// orElseGet(supplier) — the lambda only runs when the box is empty.
// Use this when the default is expensive to create.
System.out.println("orElseGet: " + empty.orElseGet(() -> "Lazy default"));
// orElseThrow — turn "missing" into a clear exception of your choosing.
try {
empty.orElseThrow(() -> new IllegalStateException("No name set!"));
} catch (IllegalStateException e) {
System.out.println("orElseThrow caught: " + e.getMessage());
}
// PROOF that orElse is eager and orElseGet is lazy:
// even though "name" is present, orElse still CALLS build()...
name.orElse(build("orElse")); // build() runs (wasteful)
name.orElseGet(() -> build("orElseGet")); // build() is SKIPPED
}
static String build(String who) {
System.out.println(" build() ran for " + who);
return "x";
}
}get() on present: Alice
orElse: Guest
orElseGet: Lazy default
orElseThrow caught: No name set!
build() ran for orElse🎯 Your Turn #1 — Create & Unwrap
Fill in the three blanks to build two Optionals and unwrap one with a safe fallback. The expected output is in the comments so you can check yourself.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
// 🎯 YOUR TURN — fill in the blanks marked with ___
// 1) Wrap a value that is GUARANTEED non-null
Optional<String> city = ___; // 👉 use Optional.of with "Paris"
// 2) Make an Optional that might be null (the value below could be null)
String raw = null;
Optional<String> maybe = ___; // 👉 use Optional.ofNullable(raw)
// 3) Unwrap "maybe" with a safe fallback of "Unknown"
String result = maybe.___("Unknown"); // 👉 use orElse
System.out.println("City: " + city.orElse("?"));
System.out.println("Maybe: " + result);
// ✅ Expected output:
// City: Paris
// Maybe: Unknown
}
}3️⃣ Chaining — map, flatMap & filter
The real power of Optional isn't checking — it's chaining. These methods let you transform a value through several steps; if the box is empty at any point, the whole chain quietly stays empty (no null checks, no NPE).
map(fn)— applyfnto the value inside the box; empty stays empty. Use whenfnreturns a plain value.flatMap(fn)— like map, but for whenfnitself returns anOptional. It flattensOptional<Optional<T>>down toOptional<T>.filter(test)— keep the value only if it passestest; otherwise the box becomes empty.
Rule of thumb: if your transforming method's return type already starts with Optional, reach for flatMap. Otherwise use map.
import java.util.Optional;
public class Main {
record Address(String city) {}
record User(String name, Address address) {}
// findCity returns an Optional, so it must be flatMap-ed, not map-ed.
static Optional<String> findCity(User u) {
return Optional.ofNullable(u.address()).map(Address::city);
}
public static void main(String[] args) {
Optional<User> user = Optional.of(new User("Alice", new Address("London")));
// map() — transform the value INSIDE the box. Empty stays empty.
Optional<String> upper = user.map(User::name).map(String::toUpperCase);
System.out.println("map: " + upper.orElse("?")); // ALICE
// flatMap() — when your function already returns an Optional, flatMap
// flattens Optional<Optional<String>> down to Optional<String>.
Optional<String> city = user.flatMap(Main::findCity);
System.out.println("flatMap: " + city.orElse("?")); // London
// filter() — keep the value only if it passes the test, else go empty.
Optional<Integer> age = Optional.of(25);
System.out.println("adult? " + age.filter(n -> n >= 18).isPresent()); // true
System.out.println("senior? " + age.filter(n -> n >= 65).isPresent()); // false
// The whole point: one safe chain, no null checks, no NPE.
String result = Optional.of(new User("Bob", null))
.flatMap(Main::findCity) // address is null -> empty
.map(c -> "Lives in " + c)
.orElse("City unknown");
System.out.println(result); // City unknown
}
}map: ALICE
flatMap: London
adult? true
senior? false
City unknown🎯 Your Turn #2 — Build a Safe Chain
Transform, filter, then react — all without unwrapping the box. Replace each ___ with the right method name.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
// 🎯 YOUR TURN — fill in the blanks marked with ___
Optional<String> input = Optional.of("hello");
// 1) Transform the value to UPPERCASE without unwrapping the box
Optional<String> shouted = input.___(String::toUpperCase); // 👉 use map
// 2) Keep it only if it is longer than 3 characters, else go empty
Optional<String> longEnough = shouted.___(s -> s.length() > 3); // 👉 use filter
// 3) Run code only if a value survived the filter
longEnough.___(s -> System.out.println("Result: " + s)); // 👉 use ifPresent
// ✅ Expected output:
// Result: HELLO
}
}4️⃣ Reacting to the Value — ifPresent & ifPresentOrElse
Sometimes you don't want a value back — you just want to do something when one exists. ifPresent(consumer) runs your code only when the box has a value, and does nothing when it's empty. There's no "else" branch.
When you need to handle both cases, ifPresentOrElse(consumer, runnable) (Java 9+) takes a second lambda that runs when the box is empty — a clean replacement for the verbose if (opt.isPresent()) { … } else { … }.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> name = Optional.of("Alice");
Optional<String> empty = Optional.empty();
// ifPresent(consumer) — run code ONLY when a value exists. No else.
name.ifPresent(n -> System.out.println("Hi, " + n + "!")); // Hi, Alice!
empty.ifPresent(n -> System.out.println("never runs")); // (nothing)
// ifPresentOrElse(consumer, runnable) — handle BOTH branches (Java 9+).
name.ifPresentOrElse(
n -> System.out.println("Found: " + n), // present
() -> System.out.println("Nobody home") // empty
);
empty.ifPresentOrElse(
n -> System.out.println("Found: " + n),
() -> System.out.println("Nobody home") // this runs
);
}
}Hi, Alice!
Found: Alice
Nobody home5️⃣ Where Optional Belongs (and where it doesn't)
Optional was designed for one job: a method return type for "there might be no result." Used anywhere else, it tends to make code worse, not better.
✅ Do
- Return
Optional<User>from afindById/ lookup. - Chain
map/flatMap/filterfor transforms. - Use
orElseGetfor expensive defaults.
❌ Don't
- Use
Optionalas a field — it's not serialisable and wastes memory. - Use
Optionalas a parameter — overload the method instead. - Return
Optional<List>— return an empty list.
🏆 Mini-Challenge — Discount Code Reader
No blanks this time — just a comment outline. Combine everything: wrap a possibly-null value, transform it, filter it by a rule, and fall back to a default. Match the expected output exactly.
import java.util.Optional;
public class Main {
record Product(String name, String discountCode) {}
// 🎯 MINI-CHALLENGE: Safely read a discount code
// 1. A product's discountCode may be null. Wrap it with Optional.ofNullable.
// 2. map it to uppercase, then filter to keep only codes that start with "SAVE".
// 3. Use orElse to fall back to "NO-DISCOUNT" when there is no valid code.
// 4. Print the final code for each product below.
//
// ✅ Expected output:
// Phone: SAVE20
// Cable: NO-DISCOUNT
// Mouse: NO-DISCOUNT
public static void main(String[] args) {
Product[] products = {
new Product("Phone", "save20"), // valid -> SAVE20
new Product("Cable", null), // null -> NO-DISCOUNT
new Product("Mouse", "deal10"), // wrong prefix -> NO-DISCOUNT
};
// your code here
}
}Common Errors (and the fix)
- ❌ Calling get() without checking:
opt.get()throwsjava.util.NoSuchElementException: No value presenton an empty box. Fix: useorElse,orElseGet,orElseThrow,map, orifPresentso the empty case is always handled. - ❌ Optional.of(null): throws
NullPointerExceptionthe instant it runs, becauseofdemands a non-null value. Fix: useOptional.ofNullable(x)wheneverxmight be null. - ❌ orElse with an expensive default:
orElse(loadFromDb())callsloadFromDb()every time, even when the value is present. Fix: useorElseGet(() -> loadFromDb())so it only runs when empty. - ❌ if (opt.isPresent()) opt.get(): this is just a wordy null check that defeats the purpose. Fix: use
opt.map(...),opt.ifPresent(...), oropt.orElse(...)instead. - ❌ Optional as a field or parameter: e.g.
private Optional<String> name;orvoid f(Optional<Order> o). Both fight Java's design (no serialisation, extra wrapping). Fix: keep the field plain and document nullability; for parameters, use method overloads.
Quick Reference
| Method | What it does | Use when |
|---|---|---|
| Optional.of(x) | Box a non-null value | x is never null (throws if it is) |
| Optional.ofNullable(x) | Box x, or empty if null | x might be null |
| Optional.empty() | An empty box | Deliberately "nothing" |
| isPresent() / isEmpty() | boolean check | Peek without unwrapping |
| get() | Unwrap (risky) | Avoid — prefer orElse* |
| orElse(T) | Value or eager default | Default is cheap |
| orElseGet(Supplier) | Value or lazy default | Default is expensive |
| orElseThrow(...) | Value or throw | Missing value is an error |
| map(Function) | Transform inner value | fn returns a plain value |
| flatMap(Function) | Transform & flatten | fn returns an Optional |
| filter(Predicate) | Keep value if it matches | Conditionally empty the box |
| ifPresent / ifPresentOrElse | Run a side effect | One or both branches |
Frequently Asked Questions
When should I use Optional and when should I not?
Use Optional as the RETURN TYPE of a method that might have no result, such as findById or a config lookup. Do NOT use it for class fields, method parameters, or collections — an empty list or array is clearer and cheaper than Optional<List<T>>. Optional is a 'might not exist' return wrapper, not a general null replacement.
What is the difference between orElse and orElseGet?
orElse(value) always evaluates its argument, even when the Optional has a value — so the default is built every time. orElseGet(supplier) only runs the lambda when the Optional is empty. If the default is expensive (a database call, a new object, a network request), use orElseGet to avoid wasted work.
Why is calling get() considered dangerous?
get() returns the value but throws NoSuchElementException if the Optional is empty — which is exactly the NullPointerException you were trying to avoid, just with a different name. Prefer orElse, orElseGet, orElseThrow, map, or ifPresent so the empty case is always handled.
What is the difference between map and flatMap?
Use map when your transforming function returns a plain value (Optional<String> -> Optional<Integer>). Use flatMap when your function itself returns an Optional; flatMap flattens the result so you get Optional<T> instead of a nested Optional<Optional<T>>.
Is Optional only for avoiding null?
Mostly, yes — it makes the absence of a value explicit in the type system so callers cannot forget to handle it. It also enables clean, null-check-free chains with map, flatMap, and filter. It does not make code faster; its value is safety and readability, not performance.
🎉 Lesson Complete!
You can now model missing values honestly instead of hiding them behind null. You know how to build Optionals with of/empty/ofNullable, unwrap them safely with orElse/orElseGet/orElseThrow, chain transforms with map/flatMap/filter, react with ifPresent/ifPresentOrElse, and steer clear of the anti-patterns.
Next up: Collections Framework Internals — how ArrayList, LinkedList, and HashSet actually work under the hood.
Sign up for free to track which lessons you've completed and get learning reminders.