Using Optional Without Making Things Worse
Optional was added in Java 8 for one purpose: to let a method say, in its return type, that it might not have an answer. It is not a general purpose null wrapper, and most of the complaints about it come from using it as one.
The problem it solves
User user = repository.findByEmail(email);
user.getName(); // maybe a NullPointerException, the signature will not say
Nothing in that signature tells you null is possible. You find out from the Javadoc, or from production.
Optional<User> user = repository.findByEmail(email);
Now the compiler will not let you skip the question. That is the whole value proposition, and it is worth having.
Stop calling get
Optional<User> user = repository.findByEmail(email);
if (user.isPresent()) {
return user.get().getName();
}
return "unknown";
This is a null check with more typing. Every isPresent followed by get can be written as one call:
return repository.findByEmail(email)
.map(User::getName)
.orElse("unknown");
The methods worth knowing:
maptransforms the value if there is one.flatMapdoes the same when the function itself returns anOptional, which avoidsOptional<Optional<T>>.filterempties the Optional when the value fails a predicate.orElsesupplies a fallback value.orElseGetsupplies it lazily.orElseThrowthrows, and is the honest way to say "this really should have been there".ifPresentOrElseruns one of two actions, for when there is no value to return.
orElse and orElseGet are not interchangeable
return repository.findByEmail(email).orElse(createGuestUser());
createGuestUser() is an argument, so it runs on every call, including the ones where a user was found. If it hits the database, sends an email or does anything else with consequences, you have a bug that no test on the happy path will catch.
return repository.findByEmail(email).orElseGet(this::createGuestUser);
The supplier runs only when the Optional is empty. Use orElse for constants and orElseGet for anything computed.
Where Optional does not belong
Not on fields. Optional is not serialisable, it costs an extra object per instance, and a field that is sometimes absent is usually a sign the class should be split. Keep the field nullable and return an Optional from the getter if the caller needs the warning.
Not as a parameter. A method taking Optional<String> forces every caller to wrap, and callers can still pass null, so you have added ceremony without removing a case. Overload the method, or accept nullable and document it.
Not as a collection. An empty list already means "nothing here". Optional<List<T>> gives callers two ways to express the same thing and forces them to handle both.
Not for a JPA entity field. Hibernate populates fields reflectively and has no idea what to do with a wrapper. It belongs on the repository method, which is where Spring Data already puts it.
Never return null from a method returning Optional
public Optional<User> find(String email) {
if (email == null) return null; // the worst of both worlds
...
}
Callers now have to null check an object whose entire purpose is to remove null checks. Return Optional.empty().
Related: Optional.of throws on null and Optional.ofNullable returns empty. Use of when null would be a bug you want to hear about immediately, and ofNullable when wrapping something legitimately absent, typically from an older API.
The stream connection
Optional and Stream share a vocabulary on purpose, and since Java 9 Optional.stream() bridges them:
List<User> users = emails.stream()
.map(repository::findByEmail)
.flatMap(Optional::stream)
.toList();
Empty Optionals contribute nothing, present ones contribute their value, and the missing entries drop out without a filter and a get.
The short version
Return it, chain it, and let it die at the end of the expression. Optional is a return type, not a field type, and if it is living in your data model rather than at the boundary of a lookup, it is doing a job it was never designed for.