Optional
Return Optional from lookups that may find nothing, and read, transform and default its value without null checks or NoSuchElementException.
Optional<T> opt = Optional.ofNullable(value);
opt.map(fn).filter(test).orElse(fallback);
opt.orElseThrow(() -> new NotFoundException(id));Optional<T> (Java 8) is a container that holds either one non-null value or nothing. Its job is to make "this method might not find anything" part of the method's signature. A method returning User could return null and nothing in the type warns the caller; a method returning Optional<User> forces them to decide what happens when there is no user.
Creating one
Optional.of(value): the value must not be null, or it throwsNullPointerExceptionimmediately.Optional.ofNullable(value): empty if the value is null. Use it at the edge where a nullable value comes in, such asmap.get(id).Optional.empty(): explicitly nothing.
Getting the value out
orElse(fallback)returns the value or a default.orElseGet(supplier)only builds the default when needed.orElseThrow()(Java 10) throwsNoSuchElementExceptionif empty;orElseThrow(() -> new NotFoundException(...))throws your own exception.ifPresent(consumer)runs code only when there is a value;ifPresentOrElse(Java 9) takes a second action for the empty case.isPresent()andisEmpty()(Java 11) answer the yes/no question.
get() also exists and throws NoSuchElementException when empty. Its own documentation now points you to orElseThrow(), which does the same thing but says so in its name.
Transforming without unwrapping
map applies a function to the value if there is one, and turns a null result into an empty Optional. filter empties it when a predicate fails. flatMap is for functions that already return an Optional, so you do not end up with Optional<Optional<T>>. or (Java 9) supplies a second Optional to try, such as cache first, then database. stream() (Java 9) gives a stream of zero or one element.
String name = repository.findById(id)
.map(User::displayName)
.orElse("unknown user");
The back-end angle
Repository lookups are the classic case. Spring Data's findById returns Optional, and a REST handler turns empty into a 404 with orElseThrow and an exception handler, or into a response directly with map and orElse. Query parameters and headers are another: they may be missing, and an Optional chain parses, validates and defaults them in one expression.
An Optional variable should never be null itself. A method declared to return Optional that returns null defeats the whole point and causes exactly the exception callers thought they were safe from.
Example
When to use it
- Return Optional<User> from a repository findById and map an empty result to a 404 response.
- Read an optional query parameter such as page size, parse it, cap it and default it in one chain.
- Look up a value in a cache first and fall back to the database with Optional.or.
More examples
of, ofNullable and empty
ofNullable is the safe entry point for values that may be missing, while of insists on a value. Every empty Optional is equal to every other.
void main() {
String header = null;
Optional<String> a = Optional.ofNullable(header);
IO.println(a.isEmpty());
Optional<String> b = Optional.of("application/json");
IO.println(b.isPresent() + " " + b.orElseThrow());
try {
Optional.of(header);
} catch (NullPointerException e) {
IO.println("Optional.of(null) threw " + e.getClass().getSimpleName());
}
IO.println(Optional.empty().equals(a));
}
// Output:
// true
// true application/json
// Optional.of(null) threw NullPointerException
// trueorElseThrow with your own exception
Throwing a specific exception lets one error handler map every not-found case to a 404. The no-argument form throws NoSuchElementException, which is better for values that must exist.
class NotFoundException extends RuntimeException {
NotFoundException(String message) {
super(message);
}
}
record Order(String id, long totalPence) {}
final Map<String, Order> orders = Map.of("o-1", new Order("o-1", 4599));
Optional<Order> findOrder(String id) {
return Optional.ofNullable(orders.get(id));
}
String handleGet(String id) {
try {
Order order = findOrder(id).orElseThrow(() -> new NotFoundException("order " + id + " not found"));
return "200 total=" + order.totalPence();
} catch (NotFoundException e) {
return "404 " + e.getMessage();
}
}
void main() {
IO.println(handleGet("o-1"));
IO.println(handleGet("o-9"));
try {
Optional.empty().orElseThrow();
} catch (NoSuchElementException e) {
IO.println("no-arg orElseThrow: " + e.getMessage());
}
}
// Output:
// 200 total=4599
// 404 order o-9 not found
// no-arg orElseThrow: No value presentflatMap, filter and a default for a query parameter
Missing, unparseable and out-of-range values all fall through to the same default without a single null check. Using map instead of flatMap here would produce Optional<Optional<Integer>>.
Optional<String> param(Map<String, String> query, String name) {
return Optional.ofNullable(query.get(name));
}
Optional<Integer> parsePositive(String value) {
try {
int n = Integer.parseInt(value.strip());
return n > 0 ? Optional.of(n) : Optional.empty();
} catch (NumberFormatException e) {
return Optional.empty();
}
}
int pageSize(Map<String, String> query) {
return param(query, "size")
.flatMap(this::parsePositive) // already returns Optional, so flatMap
.filter(n -> n <= 100)
.orElse(20);
}
void main() {
IO.println(pageSize(Map.of("size", "50")));
IO.println(pageSize(Map.of("size", "abc")));
IO.println(pageSize(Map.of("size", "5000")));
IO.println(pageSize(Map.of()));
}
// Output:
// 50
// 20
// 20
// 20
Discussion