Learn Java with Tests

A TDD course for Java, in the spirit of Learn Go with Tests.

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 null from a method that says Optional. Optional.ofNullable vs Optional.of — you want the former whenever null is possible.
  • Optional is not a collection — no stream(), no forEach abuse. 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.