Working with Optionals
Reviewed & published by Brayan K
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.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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 if x isn'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 when x is 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 when x might 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"
}
}2️⃣ 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.
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.
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";
}
}🎯 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) — apply fn to the value inside the box; empty stays empty. Use when fn returns a plain value.
- flatMap(fn) — like map, but for when fn itself returns an Optional. It flattens Optional<Optional<T>> down to Optional<T>.
- filter(test) — keep the value only if it passes test; 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
}
}🎯 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
);
}
}5️⃣ 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.
- Return Optional<User> from a findById / lookup.
- Chain map/flatMap/filter for transforms.
- Use orElseGet for expensive defaults.
- Use Optional as a field — it's not serialisable and wastes memory.
- Use Optional as 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() throws java.util.NoSuchElementException: No value present on an empty box. Fix: use orElse, orElseGet, orElseThrow, map, or ifPresent so the empty case is always handled.
- ❌ Optional.of(null): throws NullPointerException the instant it runs, because of demands a non-null value. Fix: use Optional.ofNullable(x) whenever x might be null.
- ❌ orElse with an expensive default: orElse(loadFromDb()) calls loadFromDb() every time, even when the value is present. Fix: use orElseGet(() -> 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(...), or opt.orElse(...) instead.
- ❌ Optional as a field or parameter: e.g. private Optional<String> name; or void 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 |
🎉 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.
Practice quiz
Which factory throws immediately if you pass it null?
- Optional.ofNullable(x)
- Optional.empty()
- Optional.of(x)
- Optional.from(x)
Answer: Optional.of(x). Optional.of(x) demands a non-null value and throws NullPointerException if x is null. Use ofNullable when x might be null.
Which factory gives you an empty Optional when the value is null?
- Optional.ofNullable(x)
- Optional.of(x)
- Optional.nonNull(x)
- Optional.value(x)
Answer: Optional.ofNullable(x). Optional.ofNullable(x) returns an empty Optional if x is null, otherwise it boxes the value.
Why is calling get() on an Optional considered risky?
- It is slow
- It returns null
- It always throws
- It throws NoSuchElementException on an empty Optional
Answer: It throws NoSuchElementException on an empty Optional. get() throws NoSuchElementException when empty — the very crash you were avoiding. Prefer orElse/orElseGet/map/ifPresent.
What is the difference between orElse and orElseGet?
- They are identical
- orElse always evaluates its argument; orElseGet's lambda runs only when empty
- orElseGet always evaluates; orElse is lazy
- orElse throws; orElseGet does not
Answer: orElse always evaluates its argument; orElseGet's lambda runs only when empty. orElse(buildDefault()) calls buildDefault() every time, even when present. orElseGet(() -> ...) only runs the lambda when empty.
When should you use flatMap instead of map?
- When the function itself returns an Optional
- When the function returns a plain value
- When the Optional is empty
- Never — they are interchangeable
Answer: When the function itself returns an Optional. flatMap flattens Optional<Optional<T>> to Optional<T>. Use it when your transforming function already returns an Optional.
What does Optional.of(25).filter(n -> n >= 65).isPresent() return?
- true
- It throws
- false
- null
Answer: false. 25 is not >= 65, so filter empties the Optional, and isPresent() returns false.
What does map(fn) do when the Optional is empty?
- Throws an exception
- Returns an empty Optional (fn is not called)
- Returns null
- Calls fn with null
Answer: Returns an empty Optional (fn is not called). map on an empty Optional stays empty and the function is never called — that's what makes chains null-safe.
Which method runs code only when a value is present, with no else branch?
- ifPresentOrElse
- orElse
- filter
- ifPresent
Answer: ifPresent. ifPresent(consumer) runs the consumer only when a value exists and does nothing when empty.
Where is Optional designed to be used?
- As class fields
- As a method return type for 'there might be no result'
- As method parameters
- Inside collections like List<Optional<T>>
Answer: As a method return type for 'there might be no result'. Optional is meant as a return type for lookups that might find nothing. Avoid it for fields, parameters, or collections.
Instead of returning Optional<List<T>>, what should a method return when there are no items?
- Optional.empty()
- null
- An empty list
- Optional.of(null)
Answer: An empty list. An empty list is clearer and cheaper than wrapping a list in an Optional. Reserve Optional for genuinely absent single values.
Continue this course
- Previous: Lambda Expressions Deep Dive
- Next: Java Collections Framework Internals & Performance — How ArrayList, LinkedList, and HashSet work under the hood
- Quick reference: Java cheat sheet › Null & Optional
- From the blog: Java Generics Made Simple
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.