Eliminate NullPointerException with Optional — the right way to express absent values.
Published September 21, 2026
Optional<T> (Java 8+) is a small container object that either holds one non-null value or holds nothing. Its purpose is narrow: to let a method's return type say out loud "there might not be a result", so the caller is forced to decide what to do in that case instead of hitting a NullPointerException later.
That last point matters. Optional doesn't make nulls disappear from Java. It's a way of designing APIs so that absence is visible in the method signature rather than hidden in documentation.
// Without Optional: the signature says nothing about "not found"
User findUser(String id); // returns null if missing? throws? who knows
User user = findUser(id);
String city = user.getAddress().getCity(); // NPE if user or address is null
The caller has to read the docs, or the source, to learn that null is possible, and every caller has to remember to check. With Optional the contract is in the type:
Optional<User> findUser(String id); // absence is part of the contract
String city = findUser(id)
.flatMap(User::getAddress) // getAddress() also returns Optional<Address>
.map(Address::getCity)
.orElse("Unknown");
Optional<String> a = Optional.of("hello"); // value must be non-null — of(null) throws NPE
Optional<String> b = Optional.ofNullable(maybeNull); // empty if the argument is null
Optional<String> c = Optional.empty(); // explicitly empty
Use of when a null there would be a bug, so it fails fast. Use ofNullable when wrapping a value that can legitimately be null, typically from older APIs.
| Method | Returns / does | Use when |
|---|---|---|
orElse(default) | The value, or default | The default is a cheap constant |
orElseGet(() -> ...) | The value, or the supplier's result | The default is expensive to build |
orElseThrow() (Java 10+) | The value, or NoSuchElementException | Absence is a bug |
orElseThrow(() -> new X()) | The value, or your exception | Absence is a real error case (e.g. → 404) |
ifPresent(consumer) | Runs the action only if present | Side effect only when there's a value |
ifPresentOrElse(a, b) (Java 9+) | Runs one of two actions | Both branches have side effects |
get() | The value, or throws | Almost never: it's orElseThrow() with a worse name |
orElse vs orElseGet traporElse's argument is always evaluated, even when the Optional has a value, because Java evaluates method arguments before the call:
// createDefaultUser() runs on EVERY call — even when the user exists
User u1 = findUser(id).orElse(createDefaultUser());
// the supplier runs ONLY when the Optional is empty
User u2 = findUser(id).orElseGet(() -> createDefaultUser());
If createDefaultUser() hits a database or has side effects (like inserting a row), orElse silently does that work every time. This is one of the most common Optional interview questions.
map, flatMap and filter let you work on the value as if it were there. Each step is skipped automatically if it's empty:
Optional<String> email = findUser(id)
.filter(User::isActive) // empty if the user isn't active
.map(User::getEmail) // Optional<String>; empty if getEmail() returns null
.map(String::toLowerCase);
map(f): apply f to the value. If f returns null, the result is an empty Optional, not a crash.flatMap(f): use when f itself returns an Optional. Using map there would give Optional<Optional<T>>.filter(p): keep the value only if it matches p.or(() -> otherOptional) (Java 9+): fall back to another Optional, e.g. try the cache, then the database.stream() (Java 9+): a stream of zero or one element. Handy for turning Stream<Optional<T>> into Stream<T> with flatMap(Optional::stream).The designers of the JDK were explicit that Optional is meant for return types. The widely agreed rules:
Optional<User> findById(id). Spring Data repositories do exactly this.Optional isn't Serializable and adds an extra object per field. Use a nullable field and return Optional.ofNullable(field) from the getter if needed.process(Optional<User> user) forces every caller to wrap values and still allows passing null itself. Use an overload, or a nullable parameter with clear documentation.Optional<List<T>>. Return an empty list, which already means "nothing".Optional.ofNullable(x).isPresent() is just a slower x != null.For primitives, use OptionalInt, OptionalLong and OptionalDouble (what IntStream.max() returns). They avoid boxing.
// ❌ get() without checking — throws NoSuchElementException when empty
String name = findUser(id).get().getName();
// ❌ isPresent() + get() — works, but it's a null check in disguise
if (opt.isPresent()) { use(opt.get()); }
// ✅ say what you mean
opt.ifPresent(this::use);
// ❌ returning null from a method declared to return Optional — defeats the whole point
public Optional<User> find(String id) { return null; }
// ✅
public Optional<User> find(String id) { return Optional.empty(); }
// ❌ comparing Optionals with == (they're value-based objects, identity is meaningless)
if (opt == Optional.empty()) { ... }
// ✅
if (opt.isEmpty()) { ... } // Java 11+
@Service
public class UserService {
private final UserRepository repo;
public UserDto getUser(String id) {
return repo.findById(id) // Optional<User>
.map(UserDto::from)
.orElseThrow(() -> new ResourceNotFoundException("User", id)); // → 404 via @ControllerAdvice
}
}
This is the idiomatic pattern: the repository says "may not exist", the service decides that for this use case absence is an error, and the exception handler turns that into an HTTP status.
Q: Why shouldn't Optional be used for fields?
A: It isn't Serializable, so it breaks Java serialization and some frameworks. It adds an extra object allocation per field. And it doesn't prevent the field itself from being set to null. The absence contract matters at the API boundary, so keep the field nullable and expose Optional from the getter if needed.
Q: What's the difference between map and flatMap on Optional?
A: Use map when your function returns a plain value, and the result is wrapped for you. Use flatMap when your function already returns an Optional. map would nest it as Optional<Optional<T>>, and flatMap flattens it to Optional<T>. It's the same idea as Stream.map vs Stream.flatMap.
Q: Does using Optional have a performance cost?
A: A small one: an extra object per call, and lambdas for map/orElseGet. For normal service code it's negligible, and the JIT often optimizes it away. In a very hot loop over millions of elements, plain null checks or primitive OptionalInt can be measurably faster.
Q: orElseThrow() vs get(): aren't they the same?
A: They behave the same, and both throw NoSuchElementException when empty. orElseThrow() (Java 10) was added because the name get() made it too easy to forget that it can throw. Code reviewers treat get() as a warning sign. orElseThrow() makes the possible exception obvious.
Q: How do you get a list of the present values from a List<Optional<T>>?
A: list.stream().flatMap(Optional::stream).toList(). Optional.stream() (Java 9) returns a stream with zero or one element, so flatMap drops the empty ones and unwraps the rest.