Optional & Modern Java

Working with Optional Values

15 min Lesson 3 of 13

Working with Optional Values

In the previous lesson you learned what Optional is and how to create one. Now it is time to actually use the value inside — or handle the case where the value is absent. Java gives you four core extraction methods: get(), orElse(), orElseGet(), and orElseThrow(). Each has a distinct contract, cost profile, and set of situations where it belongs. Picking the wrong one is one of the most common Optional mistakes in real codebases.

get() — the dangerous shortcut

get() returns the value directly if it is present, and throws NoSuchElementException if it is empty. It looks tempting because it is concise:

Optional<String> name = Optional.of("Alice"); String value = name.get(); // "Alice" Optional<String> empty = Optional.empty(); String boom = empty.get(); // throws NoSuchElementException at runtime
Avoid get() in almost all cases. Calling get() without first calling isPresent() is semantically identical to dereferencing a nullable reference without a null-check — you just moved the problem. The whole point of Optional is to force callers to handle the absent case at the type level, and get() lets you bypass that discipline entirely. The only legitimate use is inside a branch you have already guarded with isPresent(), but even then the safer alternatives below are almost always clearer.

orElse() — provide a constant fallback

orElse(T other) returns the value if present, otherwise returns the argument you pass in:

Optional<String> maybeName = findUserName(userId); // Returns "Guest" if no name was found String displayName = maybeName.orElse("Guest");

The fallback is evaluated eagerly — the expression you pass is computed before orElse is even called, regardless of whether the Optional is empty. For a constant like "Guest" that is perfectly fine. But watch out when the fallback is expensive to compute:

// BAD: computeExpensiveDefault() is ALWAYS called, even when the Optional is full String result = maybeValue.orElse(computeExpensiveDefault()); // GOOD: use orElseGet() instead for computed fallbacks (see below)
Rule of thumb for orElse(): use it only when the fallback is a compile-time constant, an already-computed variable, or a trivially cheap expression (e.g. orElse(0), orElse("")). Anything that involves a method call belongs in orElseGet().

orElseGet() — lazy fallback via a Supplier

orElseGet(Supplier<? extends T> supplier) accepts a lambda (or method reference) and only calls it when the Optional is empty. The supplier is not invoked at all if a value is present:

Optional<String> maybeName = findUserName(userId); // The lambda is only executed when maybeName is empty String displayName = maybeName.orElseGet(() -> loadDefaultFromDatabase(userId));

This distinction matters enormously in performance-sensitive paths. Consider a cache-lookup scenario:

public String getUserLabel(long userId) { return cache.find(userId) // returns Optional<String> .orElseGet(() -> { String fresh = db.loadLabel(userId); cache.put(userId, fresh); return fresh; }); }

The database is only hit on a cache miss — exactly the semantics you want. With orElse(db.loadLabel(userId)) the database would be called on every invocation, including cache hits.

orElseGet() is also the right choice for object construction. orElse(new BigObject()) allocates every time; orElseGet(BigObject::new) allocates only on a miss. In a tight loop that difference is significant.

orElseThrow() — signal a programming contract violation

orElseThrow() (no-arg, added in Java 10) returns the value if present, otherwise throws NoSuchElementException. It is functionally similar to get(), but the name communicates intent: you are asserting that an empty Optional at this point represents a bug or a violated precondition.

The more useful overload accepts a Supplier<? extends Throwable> so you can throw a domain-specific exception:

// No-arg: throws NoSuchElementException if empty User user = userRepository.findById(id) .orElseThrow(); // With supplier: throws a domain exception — far more informative User user = userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException( "User with id " + id + " does not exist"));

The supplier overload is lazy — the exception object is only constructed when the Optional is actually empty.

When to use orElseThrow(): use it at the boundary between layers where absence is genuinely exceptional — for example, when a controller has already validated that an entity exists and the service layer should never see an empty result. It also makes stack traces far more useful than a bare get() would, because your custom message names exactly what is missing and why.

Comparing all four side by side

Here is the same scenario expressed with each method so you can see the contrast clearly:

Optional<String> opt = findSetting("theme"); // 1. get() — dangerous; omit unless already guarded String v1 = opt.get(); // 2. orElse() — eager; great for constants String v2 = opt.orElse("light"); // 3. orElseGet() — lazy; great for computed fallbacks String v3 = opt.orElseGet(() -> configService.getDefaultTheme()); // 4. orElseThrow() — asserts presence; great for contracts String v4 = opt.orElseThrow(() -> new IllegalStateException("theme setting is required"));

A realistic worked example

Consider a service that loads a user profile and constructs a display name for the UI:

public record UserProfile(String firstName, String lastName, String preferredName) {} public String buildDisplayName(long userId) { Optional<UserProfile> profile = profileRepository.findById(userId); // Use preferred name if set; fall back to first name via a lazy call; never null String name = profile .map(UserProfile::preferredName) .filter(s -> !s.isBlank()) .orElseGet(() -> profile .map(UserProfile::firstName) .orElse("Anonymous")); return name; }

Notice how the code never calls get() and never tests isPresent() manually. The logic reads as a pipeline: prefer the preferred name, fall back to first name, fall back to "Anonymous". Each fallback is only evaluated if the previous stage yielded empty.

Summary

  • get() — avoid; use only inside an isPresent() guard if nothing else fits.
  • orElse(constant) — eager evaluation; safe for constants and cheap expressions.
  • orElseGet(supplier) — lazy evaluation; use whenever the fallback involves a method call, I/O, or object construction.
  • orElseThrow(supplier) — throws on empty; use to enforce preconditions and produce meaningful error messages at layer boundaries.