Optional & Null-Safety
Express "maybe absent" with Optional, chained access, avoiding anti-patterns
The Problem with null and Why Optional Appeared
Java was long plagued by NullPointerException: is a return value null or a real object? The caller often only finds out from the docs (or a runtime crash). Java 8 introduced Optional<T> — a container for "maybe a value, maybe not" — that, in the API signature, clearly states "this return value requires you to handle the empty case".
- ✅ Best use case: as a method return value — "no user found by id", "first element when the collection is empty", "key not in the Map"
- ✅ Suits: the "maybe absent" semantics of Stream.findFirst / Map.get, etc.
- ❌ Anti-pattern: as a field (wastes memory, hard to serialize), as a method parameter (use overloading), as a collection element
- ✅ Principle: Optional expresses an "interface contract" — telling the caller "you must explicitly handle the empty case"
Creating an Optional
// CreateOptional.java
import java.util.Optional;
public class CreateOptional {
public static void main(String[] args) {
Optional<String> a = Optional.of("alice"); // the value cannot be null, or NPE
Optional<String> b = Optional.ofNullable(null); // allows null, yields empty
Optional<String> c = Optional.empty(); // explicitly empty
System.out.println(a.isPresent()); // true
System.out.println(b.isPresent()); // false
System.out.println(c.isEmpty()); // true (Java 11+)
}
}Getting the Value: orElse / orElseGet / orElseThrow
The key difference among the three is "when the fallback is computed": orElse always computes the fallback first; orElseGet calls the supplier only when empty (lazy — recommended for an expensive fallback); orElseThrow throws if there is no value.
// GetValue.java
import java.util.Optional;
public class GetValue {
static String expensiveDefault() {
System.out.println("computing default...");
return "default";
}
public static void main(String[] args) {
Optional<String> has = Optional.of("hello");
Optional<String> none = Optional.empty();
// orElse: always computes expensiveDefault() first, even if the Optional has a value
System.out.println(has.orElse(expensiveDefault())); // prints "computing..." first, then hello
// orElseGet: calls the supplier only when empty
System.out.println(has.orElseGet(GetValue::expensiveDefault)); // does not print "computing..."
System.out.println(none.orElseGet(GetValue::expensiveDefault)); // will
// orElseThrow: for the "should have a value but doesn't" exception path
try {
none.orElseThrow(() -> new IllegalStateException("user not found"));
} catch (IllegalStateException e) {
System.out.println("caught: " + e.getMessage());
}
}
}Chained Access with map / flatMap
Use case: user object -> address object -> city name, where any layer may be null. Traditional null checks become long, fragile nested ifs; an Optional chain + map/flatMap does it in one line.
// ChainNav.java
import java.util.Optional;
public class ChainNav {
record Address(String city) {}
record User(String name, Address address) {}
record Order(User buyer) {}
static Optional<Order> lookup(long id) {
return id == 1
? Optional.of(new Order(new User("Alice", new Address("Shanghai"))))
: Optional.empty();
}
public static void main(String[] args) {
// traditional nested null checks (anti-example)
Order order = lookup(1).orElse(null);
String city = null;
if (order != null && order.buyer() != null && order.buyer().address() != null) {
city = order.buyer().address().city();
}
System.out.println("old: " + city);
// chained: clear, no NPE risk
String city2 = lookup(1)
.map(Order::buyer)
.map(User::address)
.map(Address::city)
.orElse("<unknown>");
System.out.println("new: " + city2);
// a non-existent order
String city3 = lookup(999)
.map(Order::buyer)
.map(User::address)
.map(Address::city)
.orElse("<unknown>");
System.out.println("none: " + city3);
}
}map vs flatMap: use flatMap when the mapping function itself returns an Optional, otherwise use map. Otherwise you get Optional<Optional<...>>.
// FlatMap.java
import java.util.Optional;
public class FlatMap {
static Optional<String> nickname(String name) {
return name.equals("Alice") ? Optional.of("Al") : Optional.empty();
}
public static void main(String[] args) {
Optional<String> user = Optional.of("Alice");
// map would wrap it into Optional<Optional<String>>, wrong
// Optional<Optional<String>> wrong = user.map(FlatMap::nickname);
// flatMap unwraps one layer automatically
Optional<String> nick = user.flatMap(FlatMap::nickname);
System.out.println(nick.orElse("-"));
}
}ifPresent / ifPresentOrElse
Side-effect-style handling: do one thing when present, and maybe another when empty. More declarative than if (opt.isPresent()).
// IfPresent.java
import java.util.Optional;
public class IfPresent {
public static void main(String[] args) {
Optional<String> name = Optional.of("Alice");
name.ifPresent(n -> System.out.println("hello, " + n));
Optional<String> none = Optional.empty();
none.ifPresentOrElse(
n -> System.out.println("hello, " + n),
() -> System.out.println("no user")
);
}
}filter: Keep Only If the Condition Holds
// FilterOpt.java
import java.util.Optional;
public class FilterOpt {
public static void main(String[] args) {
Optional<String> name = Optional.of("");
String result = name
.filter(s -> !s.isBlank()) // the empty string is filtered out
.orElse("<empty>");
System.out.println(result); // <empty>
}
}Pairing with Stream
Use case: flatten the Optional results in a Stream into a stream of actual values; or do the next step after findFirst.
// StreamOpt.java
import java.util.List;
import java.util.Optional;
public class StreamOpt {
static Optional<Integer> parsePositive(String s) {
try {
int v = Integer.parseInt(s);
return v > 0 ? Optional.of(v) : Optional.empty();
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static void main(String[] args) {
List<String> inputs = List.of("3", "-1", "abc", "7", "0");
// parse each, drop invalid ones, sum. Optional::stream is Java 9+
int sum = inputs.stream()
.map(StreamOpt::parsePositive)
.flatMap(Optional::stream)
.mapToInt(Integer::intValue)
.sum();
System.out.println(sum); // 10
}
}In Practice: a Typical Repository Interface
Spring Data's JpaRepository.findById returns Optional<T>. Mirroring this signature makes APIs noticeably clearer — the caller is forced to handle the "not found" case.
// RepoExample.java
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class RepoExample {
record User(long id, String name, String email) {}
static class UserRepository {
private final Map<Long, User> db = new HashMap<>();
UserRepository() {
db.put(1L, new User(1, "Alice", "alice@ex.com"));
db.put(2L, new User(2, "Bob", "bob@ex.com"));
}
Optional<User> findById(long id) {
return Optional.ofNullable(db.get(id));
}
}
public static void main(String[] args) {
UserRepository repo = new UserRepository();
// 1) get the email, use a default if absent
String email = repo.findById(1)
.map(User::email)
.orElse("unknown");
System.out.println(email);
// 2) throw a business exception if absent
User u = repo.findById(99)
.orElseThrow(() -> new IllegalStateException("user 99 not found"));
System.out.println(u); // not reached; the previous line threw
}
}Anti-Patterns
- Don't make Optional a class field: wastes memory, hard to serialize, nobody does this
- Don't make Optional a method parameter: method overloading is clearer
- Don't return Optional<List<T>>: just return an empty list, List.of()
- Don't return Optional<T[]>: use an empty array
- Don't call opt.get() without a presence check: no different from throwing NullPointerException
- Don't do opt.isPresent() + opt.get(): use ifPresent / map / orElse