Chapter 9 of 18
09 — Optionals
Designing absence with Optional instead of null.
Kata: a PhoneBook whose find(String) can miss, without ever returning null.
New ideas: Optional<T>, orElse, orElseThrow, and designing absence before it surprises you.
Designing absence
Java’s historical trap: m.get("key") returns null and nothing tells you that it can. The
fix is a matter of type: a method returning Optional<String> says “I might not have a
value”, forcing callers to handle it.
public Optional<String> find(String name) {
return Optional.ofNullable(entries.get(name));
}
Test-first, across the three ways a caller consumes the result:
@Test
void findReturnsAPresentOptionalForAKnownName() {
Optional<String> found = book.find("Alice");
assertTrue(found.isPresent());
assertEquals("111", found.get());
}
@Test
void findReturnsEmptyOptionalForAnUnknownName() {
assertFalse(book.find("nobody").isPresent());
}
RED, then implement, GREEN.
Step 2 — orElse: the safe extraction
present/absent → one usable string:
public String findOrDefault(String name, String fallback) {
return find(name).orElse(fallback);
}
Two tests, one method — the contract is “give me the phone number or this default”. No null
check at the call site.
Step 3 — orElseThrow: absence becomes an exception
Sometimes missing should be loud:
public String findOrThrow(String name) {
return find(name).orElseThrow(() -> new NoSuchEntryException(name));
}
Test the failure path with assertThrows — first glimpse of chapter 10:
NoSuchEntryException exception =
assertThrows(NoSuchEntryException.class, () -> book.findOrThrow("nobody"));
assertEquals("no entry for 'nobody'", exception.getMessage());
The three consumption flows
Optional.get() + isPresent is the if-null dance. The production habit, test-driven:
.orElse(default)— value or fallback (covered above).orElseThrow(Supplier)— value or fail loudly (covered above).map(...)on the Optional, then.orElse(...)— transform only if present
These three cover the common production patterns. A “look up X, transform, or default” task
wants optional.map(...).orElse(...) — not if (x == null).
Java-specific notes
- Never return
nullfrom a method that saysOptional.Optional.ofNullablevsOptional.of— you want the former whenevernullis possible. Optionalis not a collection — nostream(), noforEachabuse. It’s a one-slot box.Map.copyOf(entries)defensively clones on construction — the record-immutability habit (ch. 05) applied to collections.
Run it
gradle test --tests "optionals.*"
Key takeaway: absence is a design decision, not an accident. Returning Optional makes the
empty case part of the contract, and the tests lock in how callers must handle it.
Next: 10 — exceptions — what assertThrows is really telling you.