Skip to main content

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

    Worked Example: The null problem and building Optionals
    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"
        }
    }
    Output
    Was a user found? false
    of("Alice")    -> isPresent? true
    empty()         -> isPresent? false
    ofNullable(null)-> isPresent? false
    ofNullable(null)-> isEmpty?  true
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    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.

    ⚠️ 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.

    Worked Example: orElse, orElseGet, orElseThrow (eager vs lazy)
    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";
        }
    }
    Output
    get() on present: Alice
    orElse: Guest
    orElseGet: Lazy default
    orElseThrow caught: No name set!
      build() ran for orElse
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

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

    Your Turn: build and unwrap an Optional
    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
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    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.

    Worked Example: map, flatMap & filter chains
    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
        }
    }
    Output
    map: ALICE
    flatMap: London
    adult?  true
    senior? false
    City unknown
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #2 — Build a Safe Chain

    Transform, filter, then react — all without unwrapping the box. Replace each ___ with the right method name.

    Your Turn: map → filter → ifPresent
    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
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    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 { … }.

    Worked Example: ifPresent & ifPresentOrElse
    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
            );
        }
    }
    Output
    Hi, Alice!
    Found: Alice
    Nobody home
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    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.

    ✅ Do

    • Return Optional<User> from a findById / lookup.
    • Chain map/flatMap/filter for transforms.
    • Use orElseGet for expensive defaults.

    ❌ Don't

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

    Mini-Challenge: ofNullable → map → filter → orElse
    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
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    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

    MethodWhat it doesUse when
    Optional.of(x)Box a non-null valuex is never null (throws if it is)
    Optional.ofNullable(x)Box x, or empty if nullx might be null
    Optional.empty()An empty boxDeliberately "nothing"
    isPresent() / isEmpty()boolean checkPeek without unwrapping
    get()Unwrap (risky)Avoid — prefer orElse*
    orElse(T)Value or eager defaultDefault is cheap
    orElseGet(Supplier)Value or lazy defaultDefault is expensive
    orElseThrow(...)Value or throwMissing value is an error
    map(Function)Transform inner valuefn returns a plain value
    flatMap(Function)Transform & flattenfn returns an Optional
    filter(Predicate)Keep value if it matchesConditionally empty the box
    ifPresent / ifPresentOrElseRun a side effectOne 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service