Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 9 – Lambdas, functional interfaces and Optional

Java Optional done right

Chain Java Optional with map, flatMap and filter, choose orElse, orElseGet or orElseThrow from recorded call counts, and avoid the get() and field traps.

  • Intermediate
  • 16 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 16 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Transform Optional values with map, flatMap and filter
  • Choose between orElse, orElseGet, orElseThrow and ifPresentOrElse
  • Avoid common Optional anti-patterns

Before you start

What you’ll learn · Before you start

What you will learn

  • Transform Optional values with map, flatMap and filter
  • Choose between orElse, orElseGet, orElseThrow and ifPresentOrElse
  • Avoid common Optional anti-patterns

Before you start

On this page

Optional<T> holds either one value or nothing, and it exists to say so in a method’s return type: find(id) returning Optional<User> tells every caller that there may be no user, which a User that may be null never does. Using it well comes down to three habits. Transform the value inside it with map, flatMap and filter, which run only when a value is present, so a chain of lookups needs no if at all. Take the value out once, at the end, with the method whose cost and failure behaviour you want: orElse for a ready default, orElseGet for a default that is expensive to make, orElseThrow when absence is an error. Keep it out of places it was not designed for: fields, parameters and collections. The Optional Javadoc itself says that the type is meant primarily for return values. This lesson runs each habit on JDK 25, including two mistakes that only show up at run time: a default computed when nobody needed it, and an object that cannot be serialized because it holds an Optional.

A lookup chain with flatMap

The directory below can fail to find a user, a user may have no address, and an address may have no postal code on file. With null that is three checks. With Optional it is one chain:

Address.java Java · Address.java
/** postalCode may be null: not every address has one on file. */
record Address(String city, String postalCode) {}
User.java Java · User.java
import java.util.Optional;

/** address may be null; addressIfKnown() says so with an Optional. */
record User(String id, String name, Address address) {
    Optional<Address> addressIfKnown() {
        return Optional.ofNullable(address);
    }
}
Directory.java Java · Directory.java
import java.util.Map;
import java.util.Optional;

class Directory {
    private final Map<String, User> users = Map.of(
            "u1", new User("u1", "Asha", new Address("Pune", "411001")),
            "u2", new User("u2", "Ravi", null),
            "u3", new User("u3", "Meera", new Address("Kochi", null)));

    /** The user with this id, or an empty Optional. */
    Optional<User> find(String id) {
        return Optional.ofNullable(users.get(id));
    }
}
Lookup.java Java · Lookup.java
import java.util.List;
import java.util.Optional;

class Lookup {
    static String postalCode(Directory directory, String id) {
        return directory.find(id)                  // Optional<User>
                .flatMap(User::addressIfKnown)     // Optional<Address>: flatMap, because the step returns an Optional
                .map(Address::postalCode)          // Optional<String>: a null result becomes empty
                .orElse("not known");
    }

    static void main() {
        Directory directory = new Directory();
        for (String id : List.of("u1", "u2", "u3", "u9")) {
            IO.println(id + ": " + postalCode(directory, id));
        }
        // With map instead of flatMap, the step's own Optional ends up inside another one.
        Optional<Optional<Address>> nested = directory.find("u1").map(User::addressIfKnown);
        IO.println("map:     " + nested);
        IO.println("flatMap: " + directory.find("u1").flatMap(User::addressIfKnown));
        IO.println("filter:  " + directory.find("u1").filter(u -> u.name().startsWith("R")));
    }
}
find(id) then flatMap to the address then map to the postal code; any missing value turns into an empty Optional and the default.directory.find(id)Optional<User>flatMap(User::addressIfKnown)Optional<Address>map(Address::postalCode)Optional<String>orElse("not known")StringOptional.empty()the later steps are skippeda useran addressa postal codeno such userno addresspostalCode() is nullthe default

The postal-code lookup as an Optional chain

Text description of the diagram

The diagram follows the lookup chain from top to bottom.

  1. directory.find(id) returns an Optional<User>. If a user is found, the chain continues; if not, it becomes an empty Optional.
  2. flatMap(User::addressIfKnown) turns the user into an Optional<Address>. flatMap is used because addressIfKnown already returns an Optional. If the user has no address, the result is an empty Optional.
  3. map(Address::postalCode) turns the address into an Optional<String>. If postalCode() returns null, map gives an empty Optional.
  4. orElse("not known") ends the chain with a String: the postal code when every step found a value, or the default "not known" when any step produced an empty Optional. After the first empty step, none of the later steps run.
Four lookups, then map, flatMap and filter side by side Python · lookup_session.py
from session import run

run("""
java Lookup.java
""")

Output

# javac 25.0.4.1
$ java Lookup.java
u1: 411001
u2: not known
u3: not known
u9: not known
map:     Optional[Optional[Address[city=Pune, postalCode=411001]]]
flatMap: Optional[Address[city=Pune, postalCode=411001]]
filter:  Optional.empty

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lookup_session.py

Each id fails at a different step: u2 has no address, u3 has no postal code, u9 does not exist. The chain is the same for all of them, because once a step produces an empty Optional, the later steps are skipped and orElse supplies the default. The step that needed thought is the second. addressIfKnown() already returns an Optional, so map would wrap it a second time, and the session printed that nested Optional[Optional[Address[…]]]. flatMap is for steps that return an Optional themselves: it passes their result through without wrapping it again. The third step uses map on a method that may return null, which is safe, because map treats a null result as empty.

The methods that work on the value inside an Optional, and what each does with an empty one:

  • map(f) gives Optional.ofNullable(f(value)) when a value is present, and stays empty without calling f otherwise. Use it for a step that returns a plain value, which may be null.
  • flatMap(f) gives f(value), itself an Optional, when a value is present, and stays empty otherwise. Use it for a step that already returns an Optional.
  • filter(p) keeps the value only if p(value) is true, and never calls p on an empty Optional. Use it to keep a value only when it passes a test.
  • or(supplier) (Java 9) keeps a present value, and asks supplier for another Optional when empty. Use it for a second place to look.
  • stream() (Java 9) gives a stream of one element, or an empty stream. Use it to join Optionals into a stream.

Getting the value out

The ways out differ in two things: what happens when the Optional is empty, and whether the default is computed when it is not needed. The program counts calls of a deliberately slow default:

OrElse.java Java · OrElse.java
import java.util.NoSuchElementException;
import java.util.Optional;

class OrElse {
    static int loads = 0;

    /** Stands in for a slow call, such as a database query. */
    static String loadDefault() {
        loads++;
        return "guest";
    }

    static void main() {
        Optional<String> present = Optional.of("asha");
        Optional<String> empty = Optional.empty();

        loads = 0;
        present.orElse(loadDefault());
        IO.println("orElse, value present:    " + loads + " call of loadDefault()");
        loads = 0;
        present.orElseGet(() -> loadDefault());
        IO.println("orElseGet, value present: " + loads + " calls");
        loads = 0;
        String name = empty.orElseGet(() -> loadDefault());
        IO.println("orElseGet, empty:         " + loads + " call, result " + name);

        try {
            empty.orElseThrow();
        } catch (NoSuchElementException e) {
            IO.println("orElseThrow():            NoSuchElementException: " + e.getMessage());
        }
        try {
            empty.orElseThrow(() -> new IllegalStateException("no user with id u9"));
        } catch (IllegalStateException e) {
            IO.println("orElseThrow(supplier):    IllegalStateException: " + e.getMessage());
        }
        present.ifPresentOrElse(n -> IO.println("ifPresentOrElse, present: hello " + n), () -> IO.println("nobody"));
        empty.ifPresentOrElse(n -> IO.println("hello " + n), () -> IO.println("ifPresentOrElse, empty:   nobody"));
        IO.println("or, empty:                " + empty.or(() -> Optional.of("from the backup list")));
    }
}
GetEmpty.java Java · GetEmpty.java
import java.util.Optional;

class GetEmpty {
    static void main() {
        Optional<String> nickname = Optional.empty();
        IO.println(nickname.get().length());
    }
}
Call counts of each way out, then get() on an empty Optional Python · orelse_session.py
from session import run

run("""
java OrElse.java
java GetEmpty.java
""", fails=("java GetEmpty.java",))

Output

# javac 25.0.4.1
$ java OrElse.java
orElse, value present:    1 call of loadDefault()
orElseGet, value present: 0 calls
orElseGet, empty:         1 call, result guest
orElseThrow():            NoSuchElementException: No value present
orElseThrow(supplier):    IllegalStateException: no user with id u9
ifPresentOrElse, present: hello asha
ifPresentOrElse, empty:   nobody
or, empty:                Optional[from the backup list]
$ java GetEmpty.java
Exception in thread "main" java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.get(Optional.java:143)
	at GetEmpty.main(GetEmpty.java:6)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 orelse_session.py

  • orElse(value) called loadDefault() although a value was present. Java evaluates an argument before the method runs, so the default was computed and thrown away. That is harmless for a constant such as "not known", and a real cost when the default is a database query or an object that takes time to build.
  • orElseGet(supplier) takes a Supplier instead, which it calls only when the Optional is empty: 0 calls with a value present, 1 without. Use it whenever the default costs anything.
  • orElseThrow() (Java 10) throws NoSuchElementException with the message “No value present”. The overload with a supplier throws an exception of your choice, with a message that says what was missing. Use them when absence is a bug or a failed request, not a normal case.
  • ifPresentOrElse(action, emptyAction) (Java 9) runs one of two actions and returns nothing: the right tool when the value is only used for a side effect, such as printing.
  • or(supplier) (Java 9) gives another Optional when the first is empty, and keeps the result an Optional, so the chain can continue.

get() is the method to avoid. The last command shows why: on an empty Optional it throws, and the stack trace says no more than “No value present”, exactly like the NullPointerException the Optional was meant to prevent. Its Javadoc names orElseThrow() as the preferred alternative: it does the same, and its name says that it can throw. An isPresent() check followed by get() compiles and works, but it is a null check in disguise; map, orElse… and ifPresent… say the same thing in one call.

Where Optional does not belong

Misplaced.java Java · Misplaced.java
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.stream.IntStream;

/** An Optional in a field: the record compiles, but cannot be serialized. */
record Profile(String name, Optional<String> nickname) implements Serializable {}

class Misplaced {
    static void main() throws IOException {
        try (ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream())) {
            out.writeObject(new Profile("Asha", Optional.of("Ash")));
        } catch (NotSerializableException e) {
            IO.println("NotSerializableException: " + e.getMessage());
        }

        try {
            Optional<String> wrong = Optional.of(null);
        } catch (NullPointerException e) {
            IO.println("Optional.of(null): NullPointerException; use Optional.ofNullable");
        }

        OptionalInt best = IntStream.of(82, 38, 91).max();      // no Integer objects
        OptionalInt none = IntStream.empty().max();
        IO.println("best " + best.getAsInt() + ", none " + none + ", none.orElse(-1) " + none.orElse(-1));
    }
}
An Optional field, Optional.of(null), and OptionalInt Python · misplaced_session.py
from session import run

run("""
java Misplaced.java
""")

Output

# javac 25.0.4.1
$ java Misplaced.java
NotSerializableException: java.util.Optional
Optional.of(null): NullPointerException; use Optional.ofNullable
best 91, none OptionalInt.empty, none.orElse(-1) -1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 misplaced_session.py

  • Fields. Optional does not implement Serializable, so the record Profile, which does, fails at run time with NotSerializableException: java.util.Optional as soon as it holds one. The compiler did not warn. Keep the field a nullable String and return Optional.ofNullable(nickname) from the accessor, as User.addressIfKnown() does for its address.
  • Parameters. A method that takes an Optional<String> makes every caller wrap its argument, and the Optional itself can still be null. Overload the method, or accept a nullable value and document it.
  • Collections. A List<Optional<User>> is a list with gaps marked in an awkward way. Leave the missing values out, and return an empty list rather than an Optional<List<…>>.
  • null itself. Optional.of(null) throws a NullPointerException; use Optional.ofNullable. And a method that returns an Optional must never return null: the Javadoc says a variable of type Optional should always point to an Optional instance.
  • Identity. Optional is a value-based class, so never compare two of them with == or synchronise on one. Compare them with equals, or better, compare the values they hold.
  • Primitives. OptionalInt, OptionalLong and OptionalDouble hold a primitive without an Integer object. IntStream.max() returns an OptionalInt, read with getAsInt() or orElse(-1).

Optional in streams

Streams hand out Optionals where a result may be missing, findFirst, max and reduce among them, and Optional.stream() turns a present value into a one-element stream and an empty one into nothing:

Found.java Java · Found.java
import java.util.List;
import java.util.Optional;

class Found {
    static void main() {
        Directory directory = new Directory();
        List<String> ids = List.of("u1", "u9", "u3", "u7");

        // Optional.stream() turns a present value into a one-element stream and an empty one into no elements.
        List<String> names = ids.stream()
                .map(directory::find)          // Stream<Optional<User>>
                .flatMap(Optional::stream)     // Stream<User>: the empty results drop out
                .map(User::name)
                .toList();
        IO.println("found: " + names);

        Optional<String> firstWithCode = ids.stream()
                .map(directory::find)
                .flatMap(Optional::stream)
                .filter(u -> u.addressIfKnown().map(Address::postalCode).isPresent())
                .map(User::name)
                .findFirst();
        IO.println("first with a postal code: " + firstWithCode.orElse("none"));
    }
}
Dropping the empty results of a lookup with flatMap Python · found_session.py
from session import run

run("""
java Found.java
""")

Output

# javac 25.0.4.1
$ java Found.java
found: [Asha, Meera]
first with a postal code: Asha

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 found_session.py

map(directory::find) gives one Optional<User> per id, two of them empty. flatMap(Optional::stream) keeps the users that were found and drops the rest, without a single isPresent() check. The second pipeline ends with findFirst, whose Optional the program unpacks once, with orElse, at the very end.

Your turn: replace null returns with an Optional chain

managerEmailOld returns null when an employee, a manager or an email address is missing, and leaves every caller to remember it. Write managerEmail(String employee) returning Optional<String>, with the same answers. The checks compare the two methods for every case, and they count calls of the helpdesk fallback to prove that orElseGet does not build the default when a value exists.

ManagerEmail.java: one solution, with its checks Java · ManagerEmail.java
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

class ManagerEmail {
    static final Map<String, String> MANAGER = Map.of("asha", "ravi", "ravi", "meera", "kabir", "zoya");
    static final Map<String, String> EMAIL = Map.of("ravi", "Ravi@Example.com", "meera", "meera@example.com");

    /** Before: each step may return null, and every caller has to remember that. */
    static String managerEmailOld(String employee) {
        String manager = MANAGER.get(employee);
        if (manager == null) return null;
        String email = EMAIL.get(manager);
        if (email == null) return null;
        return email.toLowerCase(Locale.ROOT);
    }

    /** After: the return type says that the answer may be missing. */
    static Optional<String> managerEmail(String employee) {
        return Optional.ofNullable(MANAGER.get(employee))
                .map(EMAIL::get)                 // a manager without an email: null, so empty
                .map(email -> email.toLowerCase(Locale.ROOT));
    }

    static int helpdeskCalls = 0;

    static String helpdesk() {
        helpdeskCalls++;
        return "helpdesk@example.com";
    }

    static void check(String what, Object got, Object want) {
        IO.println((Objects.equals(got, want) ? "ok    " : "WRONG ") + what + ": " + got);
    }

    static void main() {
        check("asha's manager", managerEmail("asha"), Optional.of("ravi@example.com"));
        check("manager without an email", managerEmail("kabir"), Optional.empty());
        check("no such employee", managerEmail("nobody"), Optional.empty());
        for (String id : List.of("asha", "ravi", "kabir", "nobody")) {
            check("same answer as before for " + id, managerEmail(id).orElse(null), managerEmailOld(id));
        }
        String to = managerEmail("asha").orElseGet(ManagerEmail::helpdesk);
        check("default not computed when present", helpdeskCalls, 0);
        to = managerEmail("kabir").orElseGet(ManagerEmail::helpdesk);
        check("default computed once when empty", helpdeskCalls + " " + to, "1 helpdesk@example.com");
    }
}
The checks Python · exercise_session.py
from session import run

run("""
java ManagerEmail.java
""")

Output

# javac 25.0.4.1
$ java ManagerEmail.java
ok    asha's manager: Optional[ravi@example.com]
ok    manager without an email: Optional.empty
ok    no such employee: Optional.empty
ok    same answer as before for asha: ravi@example.com
ok    same answer as before for ravi: meera@example.com
ok    same answer as before for kabir: null
ok    same answer as before for nobody: null
ok    default not computed when present: 0
ok    default computed once when empty: 1 helpdesk@example.com

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py

Map.get returns null for a missing key, so Optional.ofNullable(MANAGER.get(employee)) starts the chain and map(EMAIL::get) turns a manager without an address into an empty Optional, because map treats null as empty. The last step needs a lambda, email -> email.toLowerCase(Locale.ROOT), because a method reference cannot pass the Locale. The two calls with orElseGet show the fallback running 0 times for Asha and once for Kabir.

Interview questions

Fresher · Why is calling get() on an Optional discouraged, and what should you call instead?

get() returns the value when one is present and throws NoSuchElementException with the message “No value present” when the Optional is empty, so calling it without a check brings back the failure Optional was meant to prevent, only with a different exception. The Javadoc names orElseThrow() as the preferred alternative: it behaves the same, and its name warns the reader that it can throw. Better still, avoid unpacking at all: transform with map, flatMap and filter, and finish with orElse for a cheap default, orElseGet for a costly one, orElseThrow with a supplier for a meaningful exception, or ifPresentOrElse when the value is only used for an action. An isPresent() check followed by get() works, but it is a null check in disguise.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A method returns cache.find(key).orElse(loadFromDatabase(key)). Metrics show the database is queried on every call, even for keys that are in the cache. Explain why, fix it, and say how a test would prove the fix.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A code review finds Optional<String> middleName as a field of a class that is serialized into a distributed cache, and service methods that take Optional parameters. What problems do you expect, and what do you recommend instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Return Optional<T> when a method may have no result; then chain with map, flatMap and filter, which skip their work on an empty Optional.
  • Use flatMap for a step that itself returns an Optional; map would nest it. map turns a null result into an empty Optional.
  • orElse always evaluates its argument; orElseGet calls its supplier only when empty. Use orElseGet for anything costly.
  • Prefer orElseThrow to get(); better still, avoid unpacking until the end of the chain.
  • Keep Optional out of fields, parameters and collections. It is not Serializable, and Optional.of(null) throws.
  • Use OptionalInt, OptionalLong and OptionalDouble for primitives, and Optional.stream() to drop empty results from a stream.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 Optional.of("asha").orElse(loadDefault()) runs once. How many times is loadDefault() called?

    Type a number.

    Show the answer to question 1

    Answer: 1

    Java evaluates method arguments before the call, so loadDefault() runs even though the value is present, and its result is thrown away. orElseGet(() -> loadDefault()) passes a Supplier instead, which is called only when the Optional is empty: 0 calls here, as the OrElse session counted.

  2. Question 2 of 5 findUser(id) returns Optional<User>, and User.addressIfKnown() returns Optional<Address>. Which call gives an Optional<Address>?

    Choose one answer.

    Show the answer to question 2

    Answer: findUser(id).flatMap(User::addressIfKnown)

    flatMap expects a function that already returns an Optional and does not wrap its result again. map would wrap it, giving an Optional<Optional<Address>>, which the Lookup session printed as Optional[Optional[Address[…]]].

  3. Question 3 of 5 What does this print?

    Read the code, then choose one answer.

    Optional<String> code = Optional.of("u3").map(id -> (String) null);
    IO.println(code.isPresent() + " " + code);
    Show the answer to question 3

    Answer: false Optional.empty

    map wraps the function's result as if with Optional.ofNullable, so a null result becomes an empty Optional. That is how the lookup chain turned Meera's missing postal code into "not known".

  4. Question 4 of 5 Where does the Optional Javadoc say the type is primarily meant to be used?

    Choose one answer.

    Show the answer to question 4

    Answer: As a method return type, when there may be no result

    Its API note calls it primarily a return type for a clear "no result". In fields it breaks serialization (the Misplaced session's NotSerializableException), as a parameter it makes every caller wrap values, and in collections an empty Optional is better left out.

  5. Question 5 of 5 A lookup may return null. Which expression wraps it safely?

    Choose one answer.

    Show the answer to question 5

    Answer: Optional.ofNullable(map.get(key))

    Optional.of throws a NullPointerException when given null, as the Misplaced session showed. ofNullable gives an empty Optional for null. empty() takes no argument, and there is no static get.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.