Optional

Intermediate
Streams & Functional Java

Use Optional<T> as an explicit nullable return type — eliminating null checks and NullPointerExceptions with a rich functional API.

Overview

Optional<T> is a container that either holds a value (non-null) or is empty. It forces callers to explicitly deal with the absent-value case, making the possibility of missing data visible in the type system. Optional should be used as a method return type — not as a field type, parameter type, or collection element. The functional API (map, flatMap, filter, ifPresent) lets you chain operations on the value without explicit null checks.

Creating & Consuming Optional

Three factory methods: Optional.of(value) — throws NullPointerException if value is null Optional.ofNullable(value) — empty if null, present otherwise Optional.empty() — explicitly absent

Consuming methods: isPresent() / isEmpty() (Java 11+) — check presence get() — get value, throws NoSuchElementException if empty — avoid! orElse(default) — return value or default orElseGet(Supplier) — lazy default — prefer over orElse for expensive defaults orElseThrow(Supplier) — throw custom exception if empty ifPresent(Consumer) — act only if value present ifPresentOrElse(Consumer, Runnable) — (Java 9+)

OptionalBasics.java
import java.util.Optional;

public class OptionalBasics {
    static Optional<String> findUserEmail(int id) {
        if (id == 1) return Optional.of("alice@example.com");
        if (id == 2) return Optional.ofNullable(null); // same as Optional.empty()
        return Optional.empty();
    }

    public static void main(String[] args) {
        Optional<String> email = findUserEmail(1);

        // isPresent / get — verbose, avoid get() without check
        if (email.isPresent()) System.out.println(email.get()); // alice@example.com

        // orElse — return default if empty
        System.out.println(findUserEmail(2).orElse("no-reply@example.com"));

        // orElseGet — lazy supplier (only called if empty)
        System.out.println(findUserEmail(99).orElseGet(() -> "generated@example.com"));

        // orElseThrow — throw if empty
        try {
            findUserEmail(99).orElseThrow(() ->
                new IllegalArgumentException("User not found"));
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage()); // User not found
        }

        // ifPresent — side-effect only if present
        findUserEmail(1).ifPresent(e -> System.out.println("Sending to: " + e));

        // ifPresentOrElse (Java 9+)
        findUserEmail(99).ifPresentOrElse(
            e  -> System.out.println("Email: " + e),
            () -> System.out.println("No email found"));
    }
}

map, flatMap & filter

Optional has the same map/flatMap/filter as Stream, but for a single element:

map(Function) — transform the value if present, otherwise stays empty flatMap(Function<T, Optional<R>>) — use when the mapper itself returns Optional (avoids Optional<Optional<T>>) filter(Predicate) — if present but fails predicate, returns empty

These let you chain nullable operations without ever writing != null.

OptionalChaining.java
import java.util.Optional;

public class OptionalChaining {
    record User(String name, Optional<Address> address) {}
    record Address(String city, Optional<String> zip) {}

    static Optional<User> findUser(int id) {
        if (id == 1) return Optional.of(
            new User("Alice", Optional.of(new Address("NYC", Optional.of("10001")))));
        if (id == 2) return Optional.of(
            new User("Bob",   Optional.empty()));
        return Optional.empty();
    }

    public static void main(String[] args) {
        // Chained map — no null checks needed
        String city = findUser(1)
            .flatMap(User::address)
            .map(Address::city)
            .orElse("Unknown city");
        System.out.println(city); // NYC

        String city2 = findUser(2)
            .flatMap(User::address)
            .map(Address::city)
            .orElse("Unknown city");
        System.out.println(city2); // Unknown city

        String city3 = findUser(99)
            .flatMap(User::address)
            .map(Address::city)
            .orElse("Unknown city");
        System.out.println(city3); // Unknown city

        // filter — stays present only if predicate passes
        Optional<String> longCity = findUser(1)
            .flatMap(User::address)
            .map(Address::city)
            .filter(c -> c.length() > 2);
        System.out.println(longCity); // Optional[NYC]

        // map vs flatMap — avoid Optional<Optional<T>>
        Optional<String> email = Optional.of("  alice@example.com  ");
        // map returns Optional<String>
        Optional<String> trimmed = email.map(String::trim);
        System.out.println(trimmed); // Optional[alice@example.com]
    }
}

Optional Best Practices

DO: • Use Optional as a method return type to signal that a value might be absent • Prefer orElseGet over orElse when the default is expensive to compute • Chain with map/flatMap instead of isPresent() + get()

DO NOT: • Use Optional as a field type — breaks serialisation, adds memory overhead • Use Optional as a parameter type — callers can pass null Optional • Put Optional in collections — use empty collection or filter nulls instead • Call get() without an isPresent() check — defeats the purpose

OptionalBestPractices.java
import java.util.*;
import java.util.stream.Stream;

public class OptionalBestPractices {
    // GOOD: Optional as return type
    static Optional<String> lookupCode(String country) {
        Map<String, String> codes = Map.of("US", "+1", "IN", "+91");
        return Optional.ofNullable(codes.get(country));
    }

    // BAD: Optional as parameter — callers can pass null
    // static void process(Optional<String> name) { ... }
    // GOOD: use overloading or null check
    static void process(String name) {
        Objects.requireNonNull(name);
        System.out.println("Processing: " + name);
    }

    public static void main(String[] args) {
        // orElseGet — supplier only called when empty
        String code = lookupCode("IN").orElseGet(() -> {
            System.out.println("Computing default..."); // not called for "IN"
            return "unknown";
        });
        System.out.println(code); // +91

        String code2 = lookupCode("XX").orElseGet(() -> {
            System.out.println("Computing default..."); // called
            return "unknown";
        });
        System.out.println(code2); // unknown

        // Optional.stream() (Java 9+) — integrate with Stream pipelines
        List<String> countries = List.of("US", "XX", "IN", "YY");
        List<String> foundCodes = countries.stream()
            .map(OptionalBestPractices::lookupCode)
            .flatMap(Optional::stream)   // discard empties, unwrap presents
            .collect(java.util.stream.Collectors.toList());
        System.out.println(foundCodes); // [+1, +91]
    }
}

Key Points to Remember

  • Optional.of() throws NPE on null; ofNullable() handles null; empty() is explicitly absent
  • Prefer orElseGet(Supplier) over orElse(value) — the supplier is lazy, value is always evaluated
  • Use map/flatMap/filter to chain Optional operations without if-else null checks
  • flatMap prevents Optional<Optional<T>> when the mapper itself returns Optional
  • Optional.stream() (Java 9+) bridges Optional into Stream pipelines cleanly
  • Never use Optional as a field, parameter, or collection element — only as a return type

Practice Optional in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the purpose of Optional in Java?

EasyAmazon
2

What is the difference between orElse() and orElseGet()?

MediumGoogle
3

What is the difference between Optional.map() and Optional.flatMap()?

MediumMicrosoft
4

Should you use Optional as a method parameter? Why or why not?

MediumOracle
5

How does Optional.stream() work in Java 9+?

MediumNetflix

Ask Aria about Optional

Your personal AI tutor — ask anything about this concept