Replacing Null Checks with Java's Optional API

Part 5 of 9 in Functional Java Unleashed

Every Java developer has written it: a cascade of if (x != null) guards that buries the real logic under defensive checks. Java’s Optional<T> was designed exactly to make those patterns more expressive — not as a magic NPE killer, but as a container type that makes absence explicit and composable.

This post walks through the four core patterns: creating Optionals safely from factory methods, transforming values with map/flatMap, providing fallbacks, and wiring Optional into stream pipelines without leaking null checks.

The code

The demo uses a layered domain model (User → Address → Province → Country) where each link may be absent. The same class also provides legacy lookup helpers that return raw null, alongside Optional-friendly versions that wrap absence in Optional.empty().

import java.util.Optional;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class OptionalPatterns {

    static record Plan(String tier, String feature) {}
    static record Subscription(Plan plan, int yearStart) {}
    static record Profile(String bio, Subscription subscription) {}

    static record User(String name, User.Address address) {
        record Address(String city, User.Province province) {}
        record Province(String name, User.Country country) {}
        record Country(String name, String code) {}
    }

    // Legacy lookup — returns null for unknown users.
    static Profile legacyLookupProfile(String username) {
        if ("bob".equals(username)) return null;
        return new Profile("engineer", new Subscription(null, 2024));
    }

    // Optional-friendly equivalent.
    static Optional<Subscription> lookupSubscriptionV2(String username) {
        return "bob".equals(username)
            ? Optional.empty()
            : Optional.ofNullable(legacyLookupProfile(username))
                .map(p -> p.subscription());
    }

    // Chained: user → subscription → plan, fully optional.
    static Optional<Plan> lookupPlan(String username) {
        return lookupSubscriptionV2(username)
            .map(s -> Optional.ofNullable(s.plan()))
            .orElse(Optional.empty());
    }

    static void demoFactories() {
        // of() — value guaranteed non-null; throws NPE otherwise
        var sure = Optional.of("hello");

        // empty() — the canonical absent sentinel
        var absent = Optional.<String>empty();

        // ofNullable() — bridges the null world safely
        String nullable = null;
        var safe = Optional.ofNullable(nullable);
    }

    static void demoTransformations() {
        // map: transform the contained value; no-op on empty
        var upper = Optional.of("alice").map(String::toUpperCase);

        // flatMap: when the transform itself returns an Optional
        var addr = Optional.of(newUser())
            .flatMap(u -> Optional.ofNullable(u.address()));
    }

    static void demoFallbacks() {
        var absent = Optional.<String>empty();
        var present = Optional.of("hello");

        // orElse — eager default (always evaluated)
        var fallback1 = absent.orElse("default");

        // orElseGet — lazy default via supplier (only called when empty)
        Supplier<String> gen = () -> "generated";
        var fallback2 = absent.orElseGet(gen);

        // orElseThrow — convert absence to error
        absent.orElseThrow(() -> new IllegalArgumentException("Missing"));

        // ifPresent — side-effect only when present
        present.ifPresent(v -> System.out.println(v));
    }

    static void demoStreamPipelines() {
        List<String> usernames = List.of("alice", "bob", "carol");

        var plans = usernames.stream()
            .map(OptionalPatterns::lookupPlan)  // Optional<Plan>
            .filter(Optional::isPresent)
            .map(Optional::get)
            .map(pl -> String.format("%s (%s)", pl.tier(), pl.feature()))
            .collect(Collectors.toList());
    }
}

Running it

The four demo methods cover each pattern section and produce this output:

=== 1. Factory methods ===

  of("hello")          -> Optional[hello]
  empty()                -> Optional.empty (isPresent: false)
  ofNullable(null)       -> Optional.empty
  ofNullable("world")   -> Optional[world]

=== 2. map and flatMap ===

  map(upper):           Optional[alice] -> Optional[ALICE]
  map on empty:         Optional.empty -> Optional.empty
    (notPresent: true)
  flatMap address:      Optional.empty
  Deep chain:           Optional[Country[name=USA, code=US]]
  Country name:         Optional[USA]

=== 3. Fallback strategies ===

  orElse("default")   -> default string
  orElseGet on empty:   generated (supplier called 1 time(s))
  orElseGet on present:  existing value (supplier called 0 time(s))
  orElseThrow:          caught IllegalArgumentException: User not found
  ifPresent:            'existing value'
    (no output above = correct behavior for empty)
  bare orElseThrow:     caught NoSuchElementException

=== 4. Stream pipeline integration ===

  Users with valid addr: 2 / 4
  City names:           [Boston, SF]

  Native-Optional stream approach:
  Valid plans (v2):     2 / 3
  Plan details:         [premium (analytics), premium (analytics)]