“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 nullIts 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");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 evaluatesdefaultValue, even when a value is present.orElseGet(supplier)calls the supplier only when empty.orElseThrow()(Java 10+) throwsNoSuchElementException;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
}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);
}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");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));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 callMonitoring 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);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.
Optionalis notSerializable, adds an allocation per field, and confuses frameworks. Store nullable fields and returnOptionalfrom getters if useful. - Optional parameters. Callers can still pass
nullfor theOptionalitself. 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());
}
}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());
}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()));
}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.