Notifications

No notifications

/Phase 4

Optional & Modern Java Idioms

Optional is Java's answer to NullPointerException — a wrapper that *forces* you to think about the absent case. It pairs perfectly with the modern Java toolbox: var, records, sealed types, switch expressions, pattern matching. This topic ties them all together so your code reads cleanly and never blows up on a stray null.

On this page

Detailed Theory

# Optional & Modern Java

Why Optional?

null says nothing about whether absence is *expected*. Optional says it loud and clear at the API boundary.

Optional<User> findById(long id);     // signature documents "may be missing"

Creating

Optional.empty();                     // no value
Optional.of(value);                   // throws NPE if value is null
Optional.ofNullable(maybeNull);       // safe wrapper for legacy null APIs

Reading — pick ONE strategy, never call get() blindly

opt.isPresent();                      // boolean
opt.ifPresent(u -> log.info(u.name()));
opt.orElse(defaultUser);              // eager default
opt.orElseGet(() -> buildDefault());  // lazy default — preferred when default is expensive
opt.orElseThrow();                    // throws NoSuchElementException
opt.orElseThrow(() -> new NotFoundException("user"));
get() exists but is essentially deprecated — it's the same as orElseThrow() and gives no clue you handled absence.

Transforming

Optional<String> name = findUser(id).map(User::name);
Optional<String> upper = name.map(String::toUpperCase);
Optional<Address> addr = findUser(id).flatMap(User::primaryAddress);
Optional<User> active = findUser(id).filter(User::isActive);
map / flatMap / filter chain just like Stream — no null checks needed.

Anti-patterns

  • Optional> — return an empty list instead.
  • Optional as a field or constructor parameter — only intended as a return type.
  • opt.get() without a guard — ticking time bomb.
  • if (opt.isPresent()) opt.get()… — use ifPresent / map / orElse instead.

Sealed classes (Java 17+)

Restrict who can extend a class — perfect for closed type hierarchies (think discriminated unions / Result types).

public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double r)             implements Shape {}
public record Square(double side)          implements Shape {}
public record Triangle(double a, double b) implements Shape {}

Switch expression + pattern matching (Java 21)

double area = switch (shape) {
    case Circle c   -> Math.PI * c.r() * c.r();
    case Square s   -> s.side() * s.side();
    case Triangle t -> 0.5 * t.a() * t.b();
};
Compiler verifies the switch is *exhaustive* over the sealed hierarchy — add a new permitted type and every switch will fail to compile until updated. Beautiful.

var (Java 10+)

var users = new ArrayList<User>();        // type inferred as ArrayList<User>
var line  = br.readLine();                // String
Rules: only on local variables with an initialiser. Don't use for nondescript types — var x = service.fetch(); hides too much.

Text blocks (Java 15+)

String json = """
    {
      "name": "alice",
      "age": 21
    }
    """;

Records + Optional in practice

public record User(long id, String name, Optional<String> email) {}

User u = new User(1, "alice", Optional.of("a@x.com")); String mail = u.email().orElse("(none)");