Skip to main content
Java intermediate Lesson 30 of 58

Optional in Java

Use Optional<T> to eliminate NullPointerException — orElse, orElseGet, orElseThrow, map, flatMap, and Optional design patterns.

The Problem Optional Solves

Before Optional, returning null to signal “no value” was the default — and easy to forget to check:

// Old style — caller may forget the null check and get NPE
public User findUser(long id) {
    // returns null if not found
    return userRepository.find(id);
}

User user = findUser(42);
System.out.println(user.getName()); // NullPointerException if not found!

Optional<T> makes the “might be absent” contract explicit in the type:

// New style — the return type signals "may not have a value"
public Optional<User> findUser(long id) {
    return userRepository.find(id)
        .map(Optional::of)
        .orElse(Optional.empty());
    // or simply: return Optional.ofNullable(userRepository.find(id));
}

Optional<User> maybeUser = findUser(42);
// Compiler forces you to handle the absent case
String name = maybeUser.map(User::getName).orElse("Unknown");

Creating Optionals

// of — value MUST be non-null (throws NPE if null)
Optional<String> present = Optional.of("Hello");

// empty — explicitly absent
Optional<String> absent = Optional.empty();

// ofNullable — wraps null as empty, non-null as present
String maybeNull = getValueThatCouldBeNull();
Optional<String> safe = Optional.ofNullable(maybeNull);

Extracting Values

isPresent / isEmpty / ifPresent

Optional<String> opt = Optional.of("Java");

if (opt.isPresent()) {
    System.out.println(opt.get()); // Java
}

// isEmpty() is the cleaner way to check (Java 11+)
if (opt.isEmpty()) {
    System.out.println("No value");
}

// ifPresent — run a Consumer only when value exists
opt.ifPresent(s -> System.out.println("Got: " + s)); // Got: Java

// ifPresentOrElse (Java 9+)
opt.ifPresentOrElse(
    s -> System.out.println("Got: " + s),
    ()  -> System.out.println("Nothing")
);

orElse, orElseGet, orElseThrow

Optional<String> empty  = Optional.empty();
Optional<String> present = Optional.of("Java");

// orElse — return fallback value (always evaluated)
String a = empty.orElse("default");   // "default"
String b = present.orElse("default"); // "Java"

// orElseGet — return result of Supplier (only evaluated when empty)
String c = empty.orElseGet(() -> computeExpensiveDefault()); // supplier called
String d = present.orElseGet(() -> computeExpensiveDefault()); // supplier NOT called

// orElseThrow — throw when empty (Java 10+)
String e = present.orElseThrow(); // "Java"
String f = empty.orElseThrow();   // NoSuchElementException

// orElseThrow with custom exception
String g = empty.orElseThrow(() -> new UserNotFoundException("User not found"));

// get() — use sparingly, throws NoSuchElementException if empty
// Prefer orElseThrow() over get() for clarity
String h = present.get(); // "Java"

Transforming with map and flatMap

map

record User(String name, String email) {}

Optional<User> maybeUser = Optional.of(new User("Alice", "alice@example.com"));

// map transforms the value if present, returns Optional<R>
Optional<String> maybeName = maybeUser.map(User::name);
System.out.println(maybeName.orElse("Anonymous")); // Alice

// Chain maps
Optional<Integer> nameLength = maybeUser
    .map(User::name)
    .map(String::length);
System.out.println(nameLength.orElse(0)); // 5

// Map on empty — returns empty without calling the function
Optional<User> noUser = Optional.empty();
Optional<String> noName = noUser.map(User::name); // empty, no NPE

flatMap

record Address(String city) {}
record Person(String name, Optional<Address> address) {}

Optional<Person> person = Optional.of(
    new Person("Alice", Optional.of(new Address("London")))
);

// Without flatMap — awkward nested Optional<Optional<Address>>
Optional<Optional<Address>> nested = person.map(Person::address);

// With flatMap — flattens one level
Optional<String> city = person
    .flatMap(Person::address)
    .map(Address::city);
System.out.println(city.orElse("Unknown")); // London

filter

Optional<Integer> score = Optional.of(75);

// filter — returns empty if predicate is false
Optional<Integer> passing = score.filter(s -> s >= 60);
passing.ifPresent(s -> System.out.println("Passed with: " + s)); // Passed with: 75

Optional<Integer> highScore = score.filter(s -> s >= 90);
System.out.println(highScore.isPresent()); // false

Optional with Streams (Java 9+)

import java.util.*;
import java.util.stream.*;

List<Optional<String>> optionals = List.of(
    Optional.of("Alice"),
    Optional.empty(),
    Optional.of("Bob"),
    Optional.empty(),
    Optional.of("Charlie")
);

// Stream over present values only (Java 9+)
List<String> names = optionals.stream()
    .flatMap(Optional::stream) // Optional.stream() returns 0 or 1 elements
    .collect(Collectors.toList());
System.out.println(names); // [Alice, Bob, Charlie]

// or() — return alternative Optional (Java 9+)
Optional<String> first = Optional.empty();
Optional<String> fallback = Optional.of("default");
Optional<String> result = first.or(() -> fallback);
System.out.println(result.get()); // default

Real-World Patterns

Repository pattern

public interface UserRepository {
    Optional<User> findById(long id);
    Optional<User> findByEmail(String email);
}

public class UserService {
    private final UserRepository repo;

    public UserService(UserRepository repo) { this.repo = repo; }

    public String getDisplayName(long userId) {
        return repo.findById(userId)
            .map(User::name)
            .orElse("Guest");
    }

    public User requireUser(long userId) {
        return repo.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("User " + userId + " not found"));
    }
}

Configuration with Optional

public class AppConfig {
    private final Properties props;

    public AppConfig(Properties props) { this.props = props; }

    public Optional<String> get(String key) {
        return Optional.ofNullable(props.getProperty(key));
    }

    public String getOrDefault(String key, String fallback) {
        return get(key).orElse(fallback);
    }

    public int getInt(String key, int fallback) {
        return get(key)
            .map(Integer::parseInt)
            .orElse(fallback);
    }
}

// Usage
AppConfig config = new AppConfig(loadProperties());
String host = config.getOrDefault("db.host", "localhost");
int port    = config.getInt("db.port", 5432);

Chained lookups

// Without Optional — deeply nested null checks
String city = null;
if (order != null && order.getCustomer() != null
        && order.getCustomer().getAddress() != null) {
    city = order.getCustomer().getAddress().getCity();
}

// With Optional — flat and readable
String city2 = Optional.ofNullable(order)
    .map(Order::getCustomer)
    .map(Customer::getAddress)
    .map(Address::getCity)
    .orElse("Unknown");

What NOT to Do

// Don't use Optional as a method parameter
// Bad
public void sendEmail(Optional<String> address) { ... }
// Good
public void sendEmail(String address) { ... }           // use nullable with docs
// or
public void sendEmail(String address) {                 // validate inside
    Objects.requireNonNull(address, "address required");
}

// Don't wrap collections in Optional
// Bad  — just return an empty list
Optional<List<String>> items = Optional.of(List.of());
// Good
List<String> items2 = List.of();

// Don't call get() without checking presence
Optional<String> opt = Optional.empty();
opt.get(); // NoSuchElementException — same problem as NullPointerException

// Don't use Optional as a field type
// Bad
class User {
    private Optional<String> nickname; // awkward serialization, no benefit
}
// Good — use nullable field with accessor that returns Optional
class User {
    private String nickname; // nullable
    public Optional<String> getNickname() { return Optional.ofNullable(nickname); }
}

Frequently Asked Questions

Should I use Optional for every field that could be null?
No. Optional is designed as a return type for methods that may or may not produce a value. Don't use it as a field type, constructor parameter, or method parameter — that's awkward and adds overhead. Use it where you want to force callers to handle the absent case explicitly.
What is the difference between orElse and orElseGet?
orElse(defaultValue) always evaluates the default expression, even if the Optional is present. orElseGet(supplier) evaluates the supplier only when the Optional is empty. Prefer orElseGet when the default is expensive to compute (database call, object construction, etc.).
Can Optional be null itself?
Technically yes — Optional is an object. But that defeats the purpose. Never return or assign null where Optional is expected. Always return Optional.empty() for the absent case. Optional.of(null) throws NullPointerException; use Optional.ofNullable(value) when value might be null.
How does Optional interact with Java Streams?
Optional.stream() (Java 9+) converts an Optional to a Stream of 0 or 1 elements, which is useful inside flatMap to filter out empty Optionals from a stream of Optionals.