What you'll learn

  • Why null causes so many bugs
  • Creating Optionals with of, ofNullable and empty
  • Chaining with map, flatMap and filter
  • Unwrapping safely with orElse, orElseGet and orElseThrow
  • Best practices and anti-patterns

NullPointerException is the most common runtime error in Java. It happens when code assumes a value exists and it doesn’t: a customer without an address, a lookup that found nothing, a missing field in an API response. Optional<T> (Java 8) makes “there might be no value” part of the type, so callers have to deal with it.

Creating Optionals

Optional<String> some  = Optional.of("value");         // must not be null
Optional<String> none  = Optional.empty();
Optional<String> maybe = Optional.ofNullable(lookup());  // null becomes empty

The most common use: return an Optional from a method when “not found” is a normal outcome.

static Optional<Customer> findCustomer(String id) {
    return Optional.ofNullable(DB.get(id));
}

Now the method’s signature tells everyone: this might not find anything.

Getting the value out — safely

findCustomer("C1").map(Customer::name).orElse("unknown");          // a default
findCustomer("C9").map(Customer::name).orElseGet(() -> "Guest-" + id);   // computed default
findCustomer("C9").orElseThrow(() -> new NoSuchElementException("Customer C9 not found"));
findCustomer("C1").ifPresent(c -> System.out.println(c.name()));
findCustomer("C9").ifPresentOrElse(
        c -> System.out.println("Found " + c.name()),
        () -> System.out.println("No such customer"));

Watch out: orElse(x) always evaluates x, even when the Optional has a value. If the default is expensive (a database call, building a big object), use orElseGet(() -> x) — it only runs when needed.

Transforming: map, flatMap, filter

The real power is chaining through data that might be missing at any step:

String city = findCustomer("C2")
        .map(Customer::address)     // null address → empty Optional
        .map(Address::city)
        .orElse("no city on file");

Without Optional, this is a pyramid of if (x != null) checks. With it, any missing link simply produces an empty result.

  • map(f) — transform the value if present (a null result becomes empty).
  • flatMap(f) — when f itself returns an Optional, to avoid Optional<Optional<T>>.
  • filter(p) — keep the value only if it matches.
  • or(() -> other) (Java 9+) — try another source if empty.
  • stream() (Java 9+) — zero or one element, perfect inside stream pipelines:
List<String> emails = ids.stream()
        .map(OptionalDemo::findCustomer)
        .flatMap(Optional::stream)          // drop the empties
        .map(Customer::email)
        .filter(Objects::nonNull)
        .toList();

Best practices

  • ✅ Use Optional as a return type for “might not exist” results.
  • ✅ Chain with map/filter/orElse instead of checking isPresent() and calling get().
  • ❌ Don’t call get() without checking — it throws NoSuchElementException, which is just a NullPointerException with a new name. Prefer orElseThrow() when absence is an error.
  • ❌ Don’t use Optional for fields or method parameters. It adds overhead and awkwardness; use null checks or overloads there.
  • ❌ Don’t return null from a method that returns Optional. Return Optional.empty().
  • ❌ Don’t wrap collections. Return an empty list instead of Optional<List<T>>.

The full example

// OptionalDemo.java — Optional instead of null.
import java.util.*;

public class OptionalDemo {

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

    static final Map<String, Customer> DB = Map.of(
            "C1", new Customer("Acme", new Address("Delhi"), "ap@acme.test"),
            "C2", new Customer("Globex", null, null));

    // Return Optional when "no result" is a normal outcome
    static Optional<Customer> findCustomer(String id) {
        return Optional.ofNullable(DB.get(id));
    }

    public static void main(String[] args) {
        // Creating Optionals
        Optional<String> some = Optional.of("value");
        Optional<String> none = Optional.empty();
        Optional<String> maybe = Optional.ofNullable(null);
        System.out.println(some.isPresent() + " " + none.isEmpty() + " " + maybe.isPresent()); // true true false

        // Getting a value safely
        System.out.println(findCustomer("C1").map(Customer::name).orElse("unknown"));   // Acme
        System.out.println(findCustomer("C9").map(Customer::name).orElse("unknown"));   // unknown

        // Chaining through possibly-missing data with map / flatMap
        String city = findCustomer("C2")
                .map(Customer::address)          // null address -> empty Optional
                .map(Address::city)
                .orElse("no city on file");
        System.out.println(city);                                                       // no city on file

        // filter
        findCustomer("C1")
                .map(Customer::email)
                .filter(e -> e.endsWith(".test"))
                .ifPresent(e -> System.out.println("Test email: " + e));

        // ifPresentOrElse (Java 9+)
        findCustomer("C9").ifPresentOrElse(
                c -> System.out.println("Found " + c.name()),
                () -> System.out.println("No such customer"));

        // orElseGet: compute the fallback only when needed
        String name = findCustomer("C9").map(Customer::name).orElseGet(() -> "Guest-" + 42);
        System.out.println(name);

        // orElseThrow: when absence is really an error
        try {
            findCustomer("C9").orElseThrow(() -> new NoSuchElementException("Customer C9 not found"));
        } catch (NoSuchElementException e) {
            System.out.println(e.getMessage());
        }

        // or (Java 9+): try another source
        Optional<Customer> c = findCustomer("C9").or(() -> findCustomer("C1"));
        System.out.println(c.map(Customer::name).get());                                 // Acme

        // stream() (Java 9+): turn many Optionals into the present values
        List<String> emails = List.of("C1", "C2", "C9").stream()
                .map(OptionalDemo::findCustomer)
                .flatMap(Optional::stream)
                .map(Customer::email)
                .filter(Objects::nonNull)
                .toList();
        System.out.println(emails);                                                       // [ap@acme.test]
    }
}

Try it yourself

Write Optional<Integer> parseQuantity(String text) that returns an empty Optional when the text isn’t a positive whole number. Then print the quantity doubled, or "invalid", for "12", "-3" and "abc".

Show a solution
static Optional<Integer> parseQuantity(String text) {
    try {
        int q = Integer.parseInt(text.trim());
        return q > 0 ? Optional.of(q) : Optional.empty();
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

for (String t : List.of("12", "-3", "abc")) {
    System.out.println(parseQuantity(t).map(q -> String.valueOf(q * 2)).orElse("invalid"));
}
// 24, invalid, invalid

Key takeaways

  • Optional makes “maybe no value” explicit in the type.
  • Create with of, ofNullable, empty; return it from lookups.
  • Chain with map, flatMap, filter; finish with orElse, orElseGet, orElseThrow or ifPresent.
  • Avoid get(), Optional fields and Optional parameters.

Example files