Skip to main content
Java · lesson 30 of 66
Open the Java page →
Java intermediate Lesson 30 of 66

Optional in Java

Learn Optional<T> by running it: create, unwrap with orElse vs orElseGet, transform with map, flatMap and filter, use it in streams, and avoid misuse.

By SkillByExample · Updated

Optional<T> is a container that holds either one value or nothing. Returning it instead of null puts “there might be no result” into the method’s signature, where the caller cannot miss it. Every example is a single file run with the JDK 21 source launcher (java File.java).

The problem: null is invisible

import java.util.Map;

public class NullProblem {
    static final Map<Integer, String> EMAILS = Map.of(1, "ada@example.com");

    static String findEmail(int userId) {
        return EMAILS.get(userId);               // null when missing
    }

    public static void main(String[] args) {
        System.out.println(findEmail(1).toUpperCase());
        System.out.println(findEmail(2).toUpperCase());
    }
}
$ java NullProblem.java
ADA@EXAMPLE.COM
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because the return value of "NullProblem.findEmail(int)" is null
	at NullProblem.main(NullProblem.java:12)

Nothing in String findEmail(int) warns that it can return null, so the caller dereferenced it. The fix is not more discipline but a return type that says so.

Creating Optionals

import java.util.Optional;

public class Creating {
    public static void main(String[] args) {
        Optional<String> some = Optional.of("value");
        Optional<String> none = Optional.empty();
        String maybe = null;
        Optional<String> fromNullable = Optional.ofNullable(maybe);

        System.out.println(some + " " + none + " " + fromNullable);
        System.out.println(some.isPresent() + " " + none.isEmpty());
        Optional.of(maybe);
    }
}
$ java Creating.java
Optional[value] Optional.empty Optional.empty
true true
Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:233)
	at java.base/java.util.Optional.of(Optional.java:113)
	at Creating.main(Creating.java:12)

Optional.of is for values you know are non-null and fails fast otherwise; ofNullable converts a possibly-null value; empty() is the explicit “no result”.

Unwrapping: orElse, orElseGet, orElseThrow

import java.util.Optional;

public class Unwrap {
    static String expensiveDefault() {
        System.out.println("  (computing default)");
        return "guest";
    }

    public static void main(String[] args) {
        Optional<String> name = Optional.of("ada");
        Optional<String> none = Optional.empty();

        System.out.println("orElse on present:");
        System.out.println(name.orElse(expensiveDefault()));
        System.out.println("orElseGet on present:");
        System.out.println(name.orElseGet(Unwrap::expensiveDefault));
        System.out.println("orElseGet on empty:");
        System.out.println(none.orElseGet(Unwrap::expensiveDefault));

        name.ifPresentOrElse(n -> System.out.println("hello " + n), () -> System.out.println("nobody"));
        none.orElseThrow(() -> new IllegalStateException("user required"));
    }
}
$ java Unwrap.java
orElse on present:
  (computing default)
ada
orElseGet on present:
ada
orElseGet on empty:
  (computing default)
guest
hello ada
Exception in thread "main" java.lang.IllegalStateException: user required
	at Unwrap.lambda$main$2(Unwrap.java:21)
	at java.base/java.util.Optional.orElseThrow(Optional.java:403)
	at Unwrap.main(Unwrap.java:21)

orElse’s argument is an ordinary expression, evaluated before the call — so the default was computed even though name had a value. orElseGet takes a supplier and only calls it when needed. orElseThrow turns absence into an exception of your choosing; without arguments it throws NoSuchElementException, which is what bare get() does too.

Transforming with map

map applies a function to the value if there is one, and passes emptiness through untouched.

import java.util.Map;
import java.util.Optional;

public class Mapping {
    static final Map<Integer, String> EMAILS = Map.of(1, "ada@example.com");

    static Optional<String> findEmail(int id) {
        return Optional.ofNullable(EMAILS.get(id));
    }

    public static void main(String[] args) {
        for (int id : new int[] {1, 2}) {
            String domain = findEmail(id)
                .map(e -> e.substring(e.indexOf('@') + 1))
                .map(String::toUpperCase)
                .orElse("(no email)");
            System.out.println(id + " -> " + domain);
        }
    }
}
$ java Mapping.java
1 -> EXAMPLE.COM
2 -> (no email)

The chain reads as the happy path, with no if (x != null) between steps. If any step’s function returns null, map turns it into an empty Optional too.

flatMap and filter

When the function itself returns an Optional, map would nest them; flatMap flattens.

import java.util.Map;
import java.util.Optional;

public class FlatMapDemo {
    record User(String name, Optional<String> managerName) {}

    static final Map<String, User> USERS = Map.of(
        "ada", new User("ada", Optional.of("grace")),
        "grace", new User("grace", Optional.empty()));

    static Optional<User> find(String name) { return Optional.ofNullable(USERS.get(name)); }

    public static void main(String[] args) {
        Optional<Optional<String>> nested = find("ada").map(User::managerName);
        System.out.println("map:     " + nested);
        System.out.println("flatMap: " + find("ada").flatMap(User::managerName));
        System.out.println("grace's: " + find("grace").flatMap(User::managerName));
        System.out.println("nobody:  " + find("bob").flatMap(User::managerName));

        System.out.println(find("ada").filter(u -> u.name().length() > 3).isPresent());
    }
}
$ java FlatMapDemo.java
map:     Optional[Optional[grace]]
flatMap: Optional[grace]
grace's: Optional.empty
nobody:  Optional.empty
false

filter keeps the value only if the predicate holds. The record field Optional<String> managerName is used here only to keep the example short — the next section explains why fields should usually not be Optional.

Optional in streams

Terminal operations that may find nothing return Optional, and Optional.stream() lets a stream of optionals drop the empty ones.

import java.util.List;
import java.util.Optional;

public class StreamOptional {
    static Optional<Integer> parse(String s) {
        try {
            return Optional.of(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }

    public static void main(String[] args) {
        List<String> raw = List.of("10", "x", "25", "", "7");
        List<Integer> nums = raw.stream().map(StreamOptional::parse).flatMap(Optional::stream).toList();
        System.out.println(nums);
        System.out.println(nums.stream().max(Integer::compare));
        System.out.println(nums.stream().filter(n -> n > 100).findFirst());
    }
}
$ java StreamOptional.java
[10, 25, 7]
Optional[25]
Optional.empty

flatMap(Optional::stream) maps each Optional to zero or one elements, so the unparseable strings simply disappear. max and findFirst return Optional because an empty stream has no answer.

What not to do

import java.util.Optional;

public class Misuse {
    static Optional<String> lookup(boolean found) {
        return found ? Optional.of("value") : Optional.empty();
    }

    public static void main(String[] args) {
        Optional<String> result = lookup(true);
        if (result.isPresent()) {                    // works, but is just a null check
            System.out.println(result.get());
        }
        System.out.println(lookup(true).orElse("none"));   // say what you mean

        Optional<String> opt = lookup(false);
        System.out.println(opt.get());
    }
}
$ java Misuse.java
value
value
Exception in thread "main" java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.get(Optional.java:143)
	at Misuse.main(Misuse.java:16)

isPresent() followed by get() is the null check in disguise, and an unguarded get() fails just like a null dereference. Prefer orElse, orElseGet, map or ifPresent. Also avoid Optional fields, parameters and collections of Optional — return an empty list instead of Optional<List<T>>.

Practice

  1. Rewrite NullProblem.findEmail to return Optional<String> and print each user’s email in lower case, or "no email on file".
Solution
import java.util.Map;
import java.util.Optional;

public class FixedEmail {
    static final Map<Integer, String> EMAILS = Map.of(1, "Ada@Example.com");

    static Optional<String> findEmail(int userId) {
        return Optional.ofNullable(EMAILS.get(userId));
    }

    public static void main(String[] args) {
        for (int id = 1; id <= 2; id++) {
            System.out.println(id + ": " + findEmail(id).map(String::toLowerCase).orElse("no email on file"));
        }
    }
}
$ java FixedEmail.java
1: ada@example.com
2: no email on file

The signature now forces the caller to choose a fallback; forgetting is no longer possible without calling get().

  1. Read a port from a config map: if the key is missing, or the value is not a number between 1 and 65535, fall back to 8080.
Solution
import java.util.Map;
import java.util.Optional;

public class Port {
    static int port(Map<String, String> config) {
        return Optional.ofNullable(config.get("port"))
            .filter(s -> s.matches("\\d+"))
            .map(Integer::parseInt)
            .filter(p -> p >= 1 && p <= 65535)
            .orElse(8080);
    }

    public static void main(String[] args) {
        System.out.println(port(Map.of("port", "9000")));
        System.out.println(port(Map.of("port", "99999")));
        System.out.println(port(Map.of("port", "abc")));
        System.out.println(port(Map.of()));
    }
}
$ java Port.java
9000
8080
8080
8080

Each filter or map that fails turns the chain empty, and one orElse at the end handles every failure case.

  1. Show that orElse runs its argument even when the value is present by counting calls to a default-making method, then switch to orElseGet and count again.
Solution
import java.util.Optional;

public class CountDefaults {
    static int calls = 0;

    static String makeDefault() {
        calls++;
        return "default";
    }

    public static void main(String[] args) {
        Optional<String> present = Optional.of("real");
        for (int i = 0; i < 3; i++) present.orElse(makeDefault());
        System.out.println("orElse calls:    " + calls);
        calls = 0;
        for (int i = 0; i < 3; i++) present.orElseGet(CountDefaults::makeDefault);
        System.out.println("orElseGet calls: " + calls);
    }
}
$ java CountDefaults.java
orElse calls:    3
orElseGet calls: 0

Java evaluates method arguments before the call, and orElse has no way to stop that.

  1. Given a list of names, find the first one starting with "Z" and print it, or print "no Z names" using ifPresentOrElse.
Solution
import java.util.List;

public class FirstZ {
    public static void main(String[] args) {
        for (List<String> names : List.of(List.of("Ann", "Zoe", "Zed"), List.of("Ann", "Bob"))) {
            names.stream()
                .filter(n -> n.startsWith("Z"))
                .findFirst()
                .ifPresentOrElse(System.out::println, () -> System.out.println("no Z names"));
        }
    }
}
$ java FirstZ.java
Zoe
no Z names

findFirst returns Optional<String>, and ifPresentOrElse handles both outcomes without an if.

Frequently Asked Questions

Should I use Optional for every field that could be null?
No. Optional was designed as a return type for methods that may have no result. Using it for fields, constructor parameters or method parameters adds allocation and awkward call sites without making the code safer. Use it where you want callers forced to deal with the empty case.
What is the difference between orElse and orElseGet?
orElse(value) evaluates its argument every time, even when the Optional holds a value. orElseGet(supplier) calls the supplier only when the Optional is empty. Prefer orElseGet when the fallback is expensive or has side effects, such as a database call or logging.
Can an Optional itself be null?
Technically yes, because Optional is an ordinary object, but returning null where an Optional is expected defeats its purpose. Return Optional.empty() for the absent case. Note that Optional.of(null) throws NullPointerException; use Optional.ofNullable when the value might be null.
How does Optional work with Java streams?
Stream operations such as findFirst, max and reduce return Optional because the stream may be empty. Since Java 9, optional.stream() turns an Optional into a stream of zero or one elements, so flatMap(Optional::stream) drops the empty results from a stream of Optionals.