Q27 of 40 · Core Java

Explain Optional<T> and when it's appropriate to use it.

Core JavaMidoptionalnull-safetyjava-fundamentalsfunctional-programming

Short answer

Short answer: Optional<T> is a container that may or may not hold a non-null value, making the possibility of absence explicit in the type system. It's appropriate as a return type when a value is genuinely optional. It should NOT be used as a field type, parameter type, or as a nullable replacement everywhere.

Detail

Optional<T> exists to make the absence of a value explicit at the type level instead of relying on null conventions. When a method returns Optional<User>, the caller cannot forget to check — the type forces them to handle both cases.

Correct use: return type of methods where absence is legitimate:

  • userRepository.findById(id)Optional<User> (user might not exist)
  • stream.findFirst()Optional<T> (stream might be empty)
  • map.getOrDefault() alternatives → Optional

Consumer-side API (preferred over isPresent() + get()):

  • orElse(T) — return wrapped value or default
  • orElseGet(Supplier<T>) — lazy default (evaluated only when empty)
  • orElseThrow() — throw NoSuchElementException if empty
  • ifPresent(Consumer) — run side effect only if present
  • map(Function) — transform the value if present, otherwise empty Optional
  • flatMap(Function<T, Optional<R>>) — avoid Optional<Optional>

Anti-patterns:

  • Field type: private Optional<String> name — adds overhead, doesn't serialise cleanly, breaks bean conventions.
  • Parameter type: void process(Optional<String> value) — callers can still pass null (an Optional itself can be null!). Use method overloading instead.
  • Using get() without isPresent(): throws NoSuchElementException — you've replaced NPE with a different exception. Always use the transformation API above.
  • Wrapping every nullable: Optional has memory overhead per instance. Don't blanket-replace all nullables.

// EXAMPLE

OptionalExample.java

import java.util.Optional;

// ✅ Return type — absence is part of the contract
public Optional<User> findUserByEmail(String email) {
    return userRepository.findByEmail(email);
    // caller MUST handle both cases
}

// ❌ get() without check — replaces NPE with NoSuchElementException
Optional<User> opt = findUserByEmail("alice@example.com");
User user = opt.get(); // throws if empty!

// ✅ Consumer API — safe, expressive
String displayName = findUserByEmail("alice@example.com")
    .map(User::getDisplayName)          // transform if present
    .orElse("Anonymous");               // default if absent

// orElseGet — lazy: supplier only called when empty
User user = findUserByEmail(email)
    .orElseGet(() -> createGuestUser()); // expensive construction avoided if present

// ifPresent — side effects only
findUserByEmail(email)
    .ifPresent(u -> auditLog.record("Login attempt: " + u.id()));

// orElseThrow — explicit failure
User required = findUserByEmail(email)
    .orElseThrow(() ->
        new IllegalStateException("Test setup: user must exist: " + email));

// flatMap — prevents Optional<Optional<T>>
Optional<String> city = findUserByEmail(email)
    .flatMap(user -> findAddress(user.id())) // returns Optional<Address>
    .map(Address::city);

// WHAT INTERVIEWERS LOOK FOR

The design intent (explicit absence in return types), the four anti-patterns (field, parameter, get() without check, blanket null replacement), and fluency with the transformation API (map, flatMap, orElseGet). Strong answers distinguish orElse (eager) from orElseGet (lazy).

// COMMON PITFALL

Using isPresent() followed by get() — this is just null-check with extra steps and negates Optional's value. The transformation methods (map, orElse, ifPresent) are the idiomatic approach and should be the default.