Skip to content
elephantoo

Optional

Lesson 32 of 43 12 min read

Represent maybe-missing values without null: map, filter, orElse, orElseThrow and good practice.


Tony Hoare, who invented the null reference, called it his "billion-dollar mistake". In Java, NullPointerException is the most common runtime error, mostly because a method returned null and the caller forgot to check. Optional<T> (Java 8+) is a container that either holds a value or is empty, making "there might be no result" explicit in a method's type.

The problem#

Java
User user = repository.findByEmail("x@example.com");   // might return null...
System.out.println(user.getName());                    // ...NullPointerException!

Nothing in the signature User findByEmail(String) warns the caller. Compare:

Java
Optional<User> findByEmail(String email);

Now the type itself says "maybe there's a user, maybe not", and the compiler won't let you treat an Optional<User> as a User.

Creating Optionals#

Creating.java
import java.util.Optional;

public class Creating {
    public static void main(String[] args) {
        Optional<String> some = Optional.of("Java");            // must not be null
        Optional<String> none = Optional.empty();
        String maybeNull = null;
        Optional<String> safe = Optional.ofNullable(maybeNull);  // empty if null

        System.out.println(some);
        System.out.println(none);
        System.out.println(safe.isPresent() + " " + safe.isEmpty());

        try {
            Optional.of(maybeNull);
        } catch (NullPointerException e) {
            System.out.println("Optional.of(null) throws NPE");
        }
    }
}
Output
Optional[Java]
Optional.empty
false true
Optional.of(null) throws NPE

Getting the value out#

Unwrapping.java
import java.util.Optional;

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

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

        System.out.println(name.orElse("guest"));
        System.out.println(missing.orElse("guest"));

        System.out.println(name.orElse(expensiveDefault()));          // default computed anyway!
        System.out.println(name.orElseGet(Unwrapping::expensiveDefault));  // not computed

        name.ifPresent(n -> System.out.println("Hello " + n));
        missing.ifPresentOrElse(
            n -> System.out.println("Hello " + n),
            () -> System.out.println("Nobody here"));

        try {
            missing.orElseThrow(() -> new IllegalStateException("User required"));
        } catch (IllegalStateException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}
Output
Asha
guest
  (computing default...)
Asha
Asha
Hello Asha
Nobody here
Error: User required
MethodBehaviour when empty
orElse(value)returns value (always evaluated)
orElseGet(supplier)calls the supplier (only when empty)
orElseThrow()throws NoSuchElementException
orElseThrow(supplier)throws your exception
ifPresent(consumer)does nothing
ifPresentOrElse(consumer, runnable)runs the runnable
get()throws NoSuchElementException: avoid it, prefer orElseThrow()

Transforming: map, flatMap, filter#

Like a stream with at most one element, Optional supports transformation without unwrapping:

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

public class Transforming {
    record Address(String city) { }
    record User(String name, Address address, String email) { }

    static final Map<Integer, User> USERS = Map.of(
        1, new User("Asha", new Address("Pune"), "asha@example.com"),
        2, new User("Ben", null, null));

    static Optional<User> findUser(int id) {
        return Optional.ofNullable(USERS.get(id));
    }

    static Optional<String> emailOf(User u) {
        return Optional.ofNullable(u.email());
    }

    public static void main(String[] args) {
        String city1 = findUser(1)
                .map(User::address)          // Optional<Address>
                .map(Address::city)          // Optional<String>
                .orElse("unknown");
        String city2 = findUser(2).map(User::address).map(Address::city).orElse("unknown");
        String city3 = findUser(99).map(User::address).map(Address::city).orElse("unknown");
        System.out.println(city1 + " " + city2 + " " + city3);

        System.out.println(findUser(1).flatMap(Transforming::emailOf).orElse("no email"));
        System.out.println(findUser(2).flatMap(Transforming::emailOf).orElse("no email"));

        Optional<User> longName = findUser(1).filter(u -> u.name().length() > 5);
        System.out.println(longName.isPresent());

        Optional<String> fallback = findUser(99).map(User::name).or(() -> Optional.of("Anonymous"));
        System.out.println(fallback.get());
    }
}
Output
Pune unknown unknown
asha@example.com
no email
false
Anonymous
  • map(fn): transform the value if present. If fn returns null, the result is empty, which makes chains through nullable fields safe.
  • flatMap(fn): use when fn itself returns an Optional, to avoid Optional<Optional<T>>.
  • filter(predicate): keep the value only if it matches.
  • or(supplier) (Java 9+): supply an alternative Optional.

Compare the null-checking version of city1:

Java
String city = "unknown";
User u = USERS.get(1);
if (u != null && u.address() != null && u.address().city() != null) {
    city = u.address().city();
}

Optional and streams#

Many stream operations return Optional, and Optional.stream() (Java 9+) turns an optional into a zero- or one-element stream, which is handy for skipping empties:

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

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

    public static void main(String[] args) {
        List<String> inputs = List.of("10", "abc", " 25 ", "", "7");

        List<Integer> valid = inputs.stream()
                .map(OptionalStreams::parse)
                .flatMap(Optional::stream)       // drop the empties
                .toList();
        System.out.println(valid);

        Optional<Integer> biggest = valid.stream().max(Integer::compare);
        System.out.println(biggest.map(n -> "Max is " + n).orElse("No numbers"));

        Optional<String> firstLong = List.of("hi", "hey").stream().filter(s -> s.length() > 5).findFirst();
        System.out.println(firstLong.orElse("none"));
    }
}
Output
[10, 25, 7]
Max is 25
none

Primitive optionals#

OptionalInt, OptionalLong and OptionalDouble avoid boxing. They are returned by IntStream.max(), average() and similar:

Java
OptionalDouble avg = IntStream.of().average();
System.out.println(avg.orElse(0.0));   // 0.0

Good and bad uses#

Do:

  • Return Optional<T> from methods that may legitimately find nothing: findById, findFirst, parse.
  • Chain map/filter/orElse instead of unwrapping early.
  • Use orElseThrow when absence is truly an error.

Don't:

  • Return null from a method declared to return Optional. That defeats the purpose.
  • Use Optional for fields, method parameters or collection elements. For collections, return an empty collection instead of Optional<List<T>>.
  • Write if (opt.isPresent()) { x = opt.get(); }. It's just a null check in disguise; use map/orElse/ifPresent.
  • Wrap everything in Optional. It's an object allocation and adds noise for values that are never missing.
Repository.java
import java.util.List;
import java.util.Optional;

public class Repository {
    record Product(String sku, String name, int stock) { }

    private final List<Product> products = List.of(
        new Product("P1", "Pen", 40), new Product("P2", "Ink", 0));

    Optional<Product> findBySku(String sku) {
        return products.stream().filter(p -> p.sku().equals(sku)).findFirst();
    }

    String availability(String sku) {
        return findBySku(sku)
                .filter(p -> p.stock() > 0)
                .map(p -> p.name() + ": " + p.stock() + " in stock")
                .orElseGet(() -> findBySku(sku).isPresent() ? sku + ": sold out" : sku + ": unknown product");
    }

    public static void main(String[] args) {
        Repository repo = new Repository();
        System.out.println(repo.availability("P1"));
        System.out.println(repo.availability("P2"));
        System.out.println(repo.availability("P9"));
    }
}
Output
Pen: 40 in stock
P2: sold out
P9: unknown product

Spring Data repositories use exactly this style: Optional<User> findById(Long id).

Common mistakes#

  • Calling get() without checking, which turns an NPE into a NoSuchElementException.
  • Using orElse(expensiveCall()) when you meant orElseGet.
  • Optional.of(possiblyNull) instead of ofNullable.
  • Comparing optionals with ==; use equals, or compare the contained values.

What's next#

Next we tackle a topic every application deals with: dates and times, using the modern java.time API.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What is the difference between Optional.of(x) and Optional.ofNullable(x)?

  2. 2.Why might opt.orElse(loadFromDatabase()) be wasteful compared with opt.orElseGet(() -> loadFromDatabase())?

  3. 3.Which is generally considered a good use of Optional?

Finished reading?

Mark this lesson complete to track your progress.