Optional

July 10, 20266 min readUpdated 8/20/2026

Optional<T> is a container that either holds a value or is empty. Its purpose is not to eliminate null — it is to move "there might be nothing here" out of your head and into the type signature, where the compiler and the next reader can both see it.

The problem it solves

record Customer(String name) { }

class Repo {
    // What does this return when there is no such customer? Nothing says.
    Customer findByIdOld(String id) {
        return null;
    }

    // This one tells you, in the signature, before you write a line
    Optional<Customer> findById(String id) {
        return Optional.empty();
    }
}

Nothing forces a caller of findByIdOld to consider the null case. The Javadoc might mention it; nobody reads it, and the NullPointerException arrives in production. The second signature makes the caller acknowledge the empty case to get at the value at all.

Creating one

Optional<String> present = Optional.of("value");        // throws if you pass null
Optional<String> empty = Optional.empty();
Optional<String> maybe = Optional.ofNullable(null);     // empty if null, otherwise present

System.out.println(present.isPresent());                 // true
System.out.println(empty.isEmpty());                     // true
System.out.println(maybe.isEmpty());                     // true

Optional.of versus ofNullable matters: of throws on null, which is what you want when the value genuinely cannot be null and a null means a bug. Use ofNullable at the boundary with code that might hand you one.

Getting the value out

There is a right way and a wrong way, and the wrong way is the one that looks most obvious:

class Demo {
    String expensiveDefault() {
        return "computed";                                 // pretend this hits a database
    }

    void run() {
        Optional<String> name = Optional.of("Folau");
        Optional<String> none = Optional.empty();

        // Wrong: get() throws NoSuchElementException on empty — you have swapped
        // one exception for another and gained nothing.
        System.out.println(name.get());

        // Right: say what happens when it is empty
        System.out.println(none.orElse("unknown"));                    // unknown
        System.out.println(none.orElseGet(this::expensiveDefault));    // lazy — only if empty
        // none.orElseThrow(() -> new IllegalStateException("required"));

        name.ifPresent(n -> System.out.println("hello " + n));         // hello Folau
        none.ifPresentOrElse(
                n -> System.out.println("hello " + n),
                () -> System.out.println("nobody here"));              // nobody here
    }
}

orElse versus orElseGet is a real distinction: orElse(expensive()) evaluates its argument every time, even when the Optional has a value, because arguments are evaluated before the call. orElseGet(() -> expensive()) runs only when empty. Use orElse for constants and orElseGet for anything computed.

If you are calling get(), you have probably missed the point. Its use is legitimate only after a check the compiler cannot see, and even then orElseThrow() with no argument reads better.

Chaining — where it actually pays off

The real benefit is not the null check; it is that map and flatMap let you keep working without unwrapping:

record Address(String city) { }
record Customer(String name, Address address) { }

class Repo {
    Optional<Customer> findById(String id) {
        return Optional.of(new Customer("Folau", new Address("Nuku'alofa")));
    }

    String cityOf(String id) {
        return findById(id)
                .map(Customer::address)          // Optional<Address>
                .map(Address::city)              // Optional<String>
                .map(String::toUpperCase)
                .orElse("unknown");              // NUKU'ALOFA
    }
}

Any step being empty short-circuits the rest and produces "unknown". The nested-null version of that method is four if statements deep.

filter works as you would expect, and flatMap is for when the function itself returns an Optional — without it you would get a Optional<Optional<T>>:

Optional<String> name = Optional.of("Folau");

System.out.println(name.filter(n -> n.length() > 3).isPresent());   // true
System.out.println(name.filter(n -> n.length() > 30).isPresent());  // false

Optional<String> nested = name.flatMap(n -> Optional.of(n.toLowerCase()));
System.out.println(nested.orElse("none"));                           // folau

System.out.println(name.stream().count());                           // 1 — bridges to streams

or, and combining two sources

A pattern that comes up constantly: try one lookup, fall back to another, and only then give up. or chains Optionals without unwrapping any of them:

class Lookup {
    Optional<String> fromCache(String key) { return Optional.empty(); }
    Optional<String> fromDatabase(String key) { return Optional.of("from-db"); }

    String resolve(String key) {
        return fromCache(key)
                .or(() -> fromDatabase(key))       // only called if the cache missed
                .orElse("not found");              // from-db
    }
}

Like orElseGet, the supplier is lazy — the database is not touched when the cache hits. Written with if statements this is six lines and an intermediate variable.

It is not a null check with extra steps

A common misuse is wrapping a value purely to test it, which is longer than the thing it replaced:

String input = "value";

// Pointless — you already have the value in hand
if (Optional.ofNullable(input).isPresent()) {
    System.out.println(input.length());
}

// Just check it
if (input != null) {
    System.out.println(input.length());
}

Optional earns its place at an API boundary — as the return type of a method whose caller genuinely might get nothing. Inside a method, where you can see the assignment three lines up, a plain null check is clearer and cheaper. Reaching for isPresent() followed by get() is the same mistake in a different shape: that pair is an if statement wearing a costume, and map, filter or ifPresent is what the type was built for.

A real one

From the console bank app this site uses for examples — the store returns an Optional rather than null:

public Optional<T> findFirst(Predicate<T> test) {
    return findAll().stream().filter(test).findFirst();
}

public Optional<T> findById(long id) {
    return findFirst(item -> idOf(item) == id);
}

And the sign-in that consumes it. This is the clearest argument for Optional in the whole app, and it is a security argument rather than a null-safety one:

public User signIn(String email, String password) {
    Optional<User> found = userStore.findByEmail(email);

    // The whole check reads as one expression: present, and the password matches.
    // Optional.filter keeps the value only if the test passes, so an unknown email and a wrong
    // password both arrive at the same empty Optional — no branch, and no way to leak which.
    return found.filter(user -> user.passwordMatches(password == null ? "" : password))
            .orElseThrow(() -> new AuthenticationException("Invalid email or password."));
}

Written with if statements, the "no such user" and "wrong password" branches are separate, and it is easy for one of them to produce a different message or return a little sooner — which tells an attacker which emails are registered. Because filter collapses both into the same empty Optional, there is only one exit and only one message.

The three places Optional does not belong

Optional was designed for one job — a return type that might have nothing — and using it elsewhere causes more problems than it solves.

1. Not as a field. It is not serialisable, it adds an object per field, and a null Optional field is the worst of both worlds:

class Bad {
    private Optional<String> nickname;      // no — and this can itself be null
}

class Good {
    private String nickname;                 // may be null internally

    Optional<String> getNickname() {         // Optional at the boundary, where callers see it
        return Optional.ofNullable(nickname);
    }
}

2. Not as a method parameter. It forces every caller to wrap, and they can still pass null. Use an overload instead — one method with the parameter, one without.

3. Not for collections. An empty List already means "nothing here". Optional<List<T>> gives callers two empty cases to handle instead of one.

Where you will meet it

List<String> names = List.of("Ana", "Bo");

System.out.println(names.stream().filter(n -> n.startsWith("B")).findFirst().orElse("none"));
System.out.println(names.stream().max(Comparator.naturalOrder()).orElse("none"));

Map<String, Integer> ages = Map.of("Ana", 30);
System.out.println(Optional.ofNullable(ages.get("Bo")).orElse(0));   // 0

The stream API returns Optional from every operation that might find nothing, which is where most people first encounter it. Note that Map.get still returns null — for compatibility — so wrapping it, or using getOrDefault, is on you.

Next

Next: forEach — iterating a collection the functional way, and the two things it cannot do that a loop can.