← All posts

Use Optional as a Clear Absence Contract

Learn to use Optional to signal potential absence and manage expensive fallbacks using lazy evaluation.

  • java-fundamentals
  • learning-series
On this page

The Contract of Absence

In Java, returning null is an ambiguous signal. It forces the caller to guess whether a null value is a legitimate result, a failure, or an uninitialized state. java.util.Optional<T> transforms this ambiguity into a type-level contract. When a method returns Optional, it explicitly tells the developer: “This value might not be here; you must decide how to handle its absence before accessing the data.”

The Danger of Blind Access

Using Optional.get() without a prior isPresent() check is essentially the same as dereferencing a null pointer, but with a different exception (NoSuchElementException). This defeats the purpose of the type. The goal is to move from “checking for null” to “defining a pipeline for the value.”

Lazy vs. Eager Fallbacks

One of the most critical distinctions in the Optional API is between orElse() and orElseGet().

  • orElse(T other): The argument is evaluated eagerly. Even if the Optional contains a value, the expression inside orElse() is executed.
  • orElseGet(Supplier<? extends T> other): The argument is evaluated lazily. The supplier function is only invoked if the Optional is empty.

In scenarios involving expensive operations—such as a database lookup or a remote API call—using orElse() can cause significant performance degradation because the fallback is computed every single time.

Worked Example: Catalog Edition Lookup

Consider a book catalog where we first look for a “Preferred Edition” (e.g., a digital version). If that is missing, we perform an expensive search for any available physical edition.

import java.util.Optional;
import java.util.logging.Logger;

public class CatalogService {
    private static final Logger logger = Logger.getLogger(CatalogService.class.getName());

    public record BookEdition(String isbn, String format) {}

    // Mocking a repository findById that returns Optional
    public Optional<BookEdition> findPreferredEdition(String bookId) {
        // Simulate a quick cache hit or miss
        return Optional.empty();
    }

    public BookEdition findAnyEditionExpensive(String bookId) {
        logger.info("Performing expensive fallback lookup for: " + bookId);
        return new BookEdition("123-456", "Hardcover");
    }

    public BookEdition getEdition(String bookId) {
        return findPreferredEdition(bookId)
            // Transform the value if present
            .map(edition -> {
                logger.info("Preferred edition found!");
                return edition;
            })
            // Lazy fallback: findAnyEditionExpensive is ONLY called if preferred is empty
            .orElseGet(() -> findAnyEditionExpensive(bookId));
    }

    public void processEdition(String bookId) {
        // Using orElseThrow to signal a business failure
        BookEdition edition = findPreferredEdition(bookId)
            .orElseThrow(() -> new RuntimeException("No edition available for " + bookId));
    }
}

Analysis of the Execution

  1. The Pipeline: findPreferredEdition returns an Optional.empty().
  2. The Map: The .map() block is skipped entirely because the Optional is empty.
  3. The Fallback: orElseGet() triggers the Supplier. The log “Performing expensive fallback lookup” appears exactly once.
  4. Failure Case: If we had used .orElse(findAnyEditionExpensive(bookId)), the expensive method would run every time, regardless of whether a preferred edition existed.

Functional Chaining with flatMap

While map transforms the value inside the Optional, flatMap is used when the transformation function itself returns an Optional. This prevents the creation of a nested Optional<Optional<T>>.

Exercise

Scenario: You have a User record. A User may have an Optional<Profile>, and a Profile may have an Optional<Address>. Write a method that retrieves the Address from a User object, returning an empty Optional if any step in the chain is missing, and throwing a CustomException if the final result is empty.

Answer:

public Optional<Address> getAddress(User user) {
    return user.getProfile() // returns Optional<Profile>
              .flatMap(Profile::getAddress); // returns Optional<Address>
}

// Usage
Address addr = getAddress(user)
    .orElseThrow(CustomException::new);

Further reading