Ch. 7 · Java

Java Optional: When to Use It and Common Mistakes

When to return Optional in Java, why get and isPresent checks miss the point, and why Optional fields, parameters and collections are a smell.

~6 min readintermediateupdated Oct 6, 2026

“When should you use Optional?” and “What is wrong with optional.get()?” are common Java 8+ interview questions. They look simple, but they reveal how a candidate designs APIs. Interviewers listen for whether you see Optional as a contract on return values or as a general-purpose null wrapper to sprinkle everywhere.

Brian Goetz, the Java language architect, has described its intended use as a limited mechanism for library method return types where there is a clear need to represent “no result”. Starting from that intent makes the rest of the answer fall into place.

Before you start

You should know what null means in Java, why NullPointerException is common, and how lambdas and method references work. Familiarity with streams helps, because Optional shares their map, filter and flatMap vocabulary, and terminal operations like findFirst and max return it.

The short answer

Return Optional<T> from a method when “no value” is an expected, legitimate result, such as looking up a user by email. It makes absence visible in the signature, so callers must decide what to do. Consume it with map, flatMap, filter, orElse, orElseGet, orElseThrow or ifPresentOrElse rather than isPresent() followed by get(). Do not use Optional for fields, method parameters or collection elements, and never return null from a method declared to return Optional.

How it works

Optional is a final, immutable container holding either one non-null value or nothing:

Optional<String> some = Optional.of("asha@example.com");   // throws NPE if argument is null
Optional<String> none = Optional.empty();
Optional<String> maybe = Optional.ofNullable(lookup());    // empty if lookup() returns null
java

Its value comes from the operations that let callers express intent without branching:

String city = userRepository.findByEmail(email)   // Optional<User>
        .map(User::address)                      // Optional<Address>, empty if address is null
        .map(Address::city)
        .orElse("Unknown");
java

map applies a function only when a value is present and wraps a null result as empty. flatMap is for functions that already return an Optional, to avoid Optional<Optional<T>>. filter turns a value that fails a predicate into empty.

The four ways to end a chain have different semantics:

  • orElse(defaultValue) always evaluates defaultValue, even when a value is present.
  • orElseGet(supplier) calls the supplier only when empty.
  • orElseThrow() (Java 10+) throws NoSuchElementException; orElseThrow(supplier) throws your exception.
  • ifPresentOrElse(action, emptyAction) (Java 9+) runs side effects.

Java 9 also added or(supplier) to fall back to another Optional and stream() to turn it into a zero-or-one element stream.

Step-by-step walkthrough

Step 1: Decide whether absence is normal

findById returning nothing is normal: the id might not exist. getCurrentUser inside an authenticated request returning nothing is a bug and should throw. Only the first deserves Optional.

public interface UserRepository {
    Optional<User> findByEmail(String email);   // absence is expected
    User getById(UserId id);                    // throws if missing
}
java

Spring Data JPA follows this convention: findById returns Optional<T>.

Step 2: Return empty, never null

public Optional<User> findByEmail(String email) {
    User u = jdbc.queryForUser(email);    // may return null
    return Optional.ofNullable(u);
}
java

A method declared to return Optional that returns null breaks the contract callers rely on, and they will get a NullPointerException from the very API meant to prevent one.

Step 3: Transform instead of unwrapping

// avoid
Optional<User> user = repo.findByEmail(email);
if (user.isPresent()) {
    return user.get().displayName();
}
return "Guest";

// prefer
return repo.findByEmail(email)
        .map(User::displayName)
        .orElse("Guest");
java

The first version is just a verbose null check, and if someone later deletes the if, get() throws NoSuchElementException.

Step 4: Choose the right terminal operation

// expensive default: use orElseGet so it runs only when needed
Config cfg = cache.find(key).orElseGet(() -> loadFromDisk(key));

// absence is an error at this point: say so
Order order = orders.findById(id)
        .orElseThrow(() -> new OrderNotFoundException(id));
java

Worked scenario

A checkout service applies a promotional discount if the customer has one:

BigDecimal discount = promotions.findActive(customerId)
        .map(Promotion::percentage)
        .orElse(fetchDefaultDiscount());   // remote call
java

Monitoring shows the default discount endpoint receiving one call per checkout, including checkouts that had a promotion. The cause is orElse: Java evaluates method arguments before the call, so fetchDefaultDiscount() runs every time. Under load, the extra calls triple latency for the discount service.

The fix uses lazy evaluation:

BigDecimal discount = promotions.findActive(customerId)
        .map(Promotion::percentage)
        .orElseGet(this::fetchDefaultDiscount);
java

A related bug in the same code base: a CustomerDto had a field Optional<String> middleName. Jackson serialized it as an object rather than a string until the Jdk8Module was registered, and JPA could not map the field at all. The cleaner fix was a plain nullable String field with an accessor that returns Optional.ofNullable(middleName), keeping Optional at the method boundary where it belongs.

Common mistake

  • isPresent followed by get. It is a null check with extra steps; use transformations or orElseThrow.
  • Optional fields. Optional is not Serializable, adds an allocation per field, and confuses frameworks. Store nullable fields and return Optional from getters if useful.
  • Optional parameters. Callers can still pass null for the Optional itself. Use overloads or a nullable parameter with clear documentation.
  • Optional of a collection. Return an empty list instead of Optional<List<T>>; empty already means “nothing”.
  • Using orElse with expensive defaults. The default is evaluated even when not used.

Verify the behavior

import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;

class OptionalTest {
    @Test
    void orElseIsEagerOrElseGetIsLazy() {
        var calls = new AtomicInteger();
        Optional<String> present = Optional.of("x");

        present.orElse(String.valueOf(calls.incrementAndGet()));
        assertEquals(1, calls.get());          // evaluated anyway

        present.orElseGet(() -> String.valueOf(calls.incrementAndGet()));
        assertEquals(1, calls.get());          // not evaluated
    }

    @Test
    void mapTurnsNullIntoEmpty() {
        Optional<String> result = Optional.of("key").map(k -> (String) null);
        assertTrue(result.isEmpty());          // Java 11+
    }

    @Test
    void orElseThrowWithoutArgument() {
        assertThrows(NoSuchElementException.class, () -> Optional.empty().orElseThrow());
    }
}
java

Follow-up questions

Why is Optional not Serializable? It was designed only for return values, not for storage in fields or transmission, so the JDK team deliberately left it out.

What are OptionalInt and friends for? OptionalInt, OptionalLong and OptionalDouble avoid boxing. IntStream.max() returns OptionalInt.

Should repositories return Optional or throw? Return Optional from find methods where missing is normal, and let the service decide whether missing is a 404 or an internal error.

How does Optional compare to Kotlin nullable types? Kotlin enforces nullability in the type system for every reference at zero runtime cost. Optional is a library type, allocates an object, and only covers the places where you choose to use it.

Interview exercise

Refactor this method so that the absence contract is explicit and the expensive fallback runs only when needed:

public String preferredLanguage(String userId) {
    User u = users.find(userId);           // may return null
    if (u != null && u.settings() != null && u.settings().language() != null) {
        return u.settings().language();
    }
    return geoService.languageFor(currentRequestIp());
}
java

Answer and reasoning

First, change the repository to Optional<User> find(String userId) so callers cannot forget the null case. Then chain the nullable steps with map, which turns any null into empty, and use orElseGet for the remote fallback:

public String preferredLanguage(String userId) {
    return users.find(userId)
            .map(User::settings)
            .map(Settings::language)
            .orElseGet(() -> geoService.languageFor(currentRequestIp()));
}
java

Each map replaces one null check. orElseGet keeps the geo lookup lazy, which preserves the original behaviour where the remote call only happened on the fallback path. Using orElse here would be a behavioural regression, which is precisely the detail interviewers look for.

Continue learning

Practise with more Java interview questions and the Java MCQs. Related notes: fixing NullPointerException, exceptions and recovery boundaries and stream laziness. Read the method contracts in the Optional API documentation.

More in Java

esc