Modern JavaFunctional Java (Java 8+)
Optional Done Right
What Optional is for, the idiomatic API, the anti-patterns to avoid and where not to use it.
Java
The purpose
Optional<T> is a container that holds either a value or nothing. It was designed for one job: as a method return type that makes “there may be no result” explicit in the signature, so callers are forced to deal with absence instead of tripping over null.
Optional<User> findByEmail(String email); // the signature tells you it can be empty
Creating Optionals
Optional.of(value); // value must be non-null, else NPE
Optional.ofNullable(maybe); // empty if null
Optional.empty();
The idiomatic API
import java.util.*;
record Address(String city, String postcode) {}
record User(String name, String email, Address address) {}
class UserRepository {
private final Map<String, User> users = Map.of(
"ada@example.com", new User("Ada", "ada@example.com", new Address("London", "N1 9GU")),
"bob@example.com", new User("Bob", "bob@example.com", null));
Optional<User> findByEmail(String email) { return Optional.ofNullable(users.get(email)); }
}
class Main {
public static void main(String[] args) {
var repo = new UserRepository();
// map / flatMap chain through possibly-missing values
String city = repo.findByEmail("ada@example.com")
.map(User::address)
.map(Address::city)
.orElse("Unknown");
System.out.println("Ada's city: " + city);
String bobCity = repo.findByEmail("bob@example.com")
.map(User::address) // null address -> empty Optional
.map(Address::city)
.orElse("Unknown");
System.out.println("Bob's city: " + bobCity);
// filter
boolean londoner = repo.findByEmail("ada@example.com")
.map(User::address).filter(a -> a.city().equals("London")).isPresent();
System.out.println("Londoner? " + londoner);
// ifPresentOrElse (Java 9)
repo.findByEmail("cy@example.com").ifPresentOrElse(
u -> System.out.println("Found " + u.name()),
() -> System.out.println("No user cy@example.com"));
// or (Java 9): fall back to another Optional-returning lookup
User user = repo.findByEmail("missing@example.com")
.or(() -> repo.findByEmail("bob@example.com"))
.orElseThrow();
System.out.println("Fallback: " + user.name());
// orElse vs orElseGet: orElse ALWAYS evaluates its argument
repo.findByEmail("ada@example.com").orElse(expensiveDefault("orElse"));
repo.findByEmail("ada@example.com").orElseGet(() -> expensiveDefault("orElseGet"));
// orElseThrow with a domain exception
try {
repo.findByEmail("nobody@example.com").orElseThrow(() -> new NoSuchElementException("User not found"));
} catch (NoSuchElementException e) { System.out.println("Thrown: " + e.getMessage()); }
// stream() (Java 9): drop empties from a list of Optionals
List<String> names = List.of("ada@example.com", "x@example.com", "bob@example.com").stream()
.map(repo::findByEmail).flatMap(Optional::stream).map(User::name).toList();
System.out.println("Found names: " + names);
}
static User expensiveDefault(String via) {
System.out.println(" expensiveDefault() called via " + via);
return new User("default", "", null);
}
}Output
Notice that orElse(...) called expensiveDefault even though the value was present. Use orElseGet(supplier) when the default is costly (a database call, object creation).
Anti-patterns
// 1. isPresent + get is just a verbose null check
Optional<User> u = repo.findById(id);
if (u.isPresent()) {
send(u.get().email());
}
// 2. Returning null from a method declared to return Optional
Optional<User> find(long id) { return null; }
// 3. Optional fields, parameters and collections
class Order { private Optional<String> note; }
void ship(Optional<Address> address) { }
Optional<List<Item>> items();
// 4. Optional.of with a value that might be null
Optional.of(map.get(key));// 1. Use the functional API
repo.findById(id).map(User::email).ifPresent(this::send);
// 2. Return Optional.empty()
Optional<User> find(long id) { return Optional.empty(); }
// 3. Nullable field + Optional getter; overloads; empty collections
class Order {
private String note; // may be null
Optional<String> note() { return Optional.ofNullable(note); }
}
void ship(Address address) { } void ship() { }
List<Item> items(); // return List.of(), never null
// 4. ofNullable
Optional.ofNullable(map.get(key));Optional in Spring
Spring Data repositories return Optional<T> from findById and from derived queries like Optional<User> findByEmail(String email). A common service-layer pattern:
public UserDto getUser(Long id) {
return userRepository.findById(id)
.map(UserDto::from)
.orElseThrow(() -> new UserNotFoundException(id)); // mapped to 404 by an @RestControllerAdvice
}
Finished this lesson?
Mark it complete to track your progress through Modern Java.