V
Vel·ToolKit
Simple · Fast · Ready to use
EN
Chapter 15 of 20

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