Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.
Creation, Extraction, and the orElse Family
// ── Creation: of, empty, ofNullable ───────────────────────────────────
Optional<String> present = Optional.of("hello");
Optional<String> absent = Optional.empty();
// of() throws immediately on null — fail-fast:
try {
Optional<String> fails = Optional.of(null);
} catch (NullPointerException e) {
System.out.println("of(null) throws immediately");
}
// ofNullable() handles uncertain nullness gracefully:
String maybeNull = getValueThatMightBeNull(); // legacy API returning null sometimes
Optional<String> safe = Optional.ofNullable(maybeNull);
System.out.println(safe.isPresent()); // depends on maybeNull
// ── isPresent / isEmpty ────────────────────────────────────────────────
System.out.println(present.isPresent()); // true
System.out.println(present.isEmpty()); // false (Java 11+)
System.out.println(absent.isPresent()); // false
System.out.println(absent.isEmpty()); // true
// ── get() / orElseThrow() — risky if not checked first ────────────────
System.out.println(present.get()); // "hello" — OK, we know it's present
try {
absent.get(); // NoSuchElementException: No value present
} catch (NoSuchElementException e) {
System.out.println("Caught: " + e.getMessage());
}
// orElseThrow() no-arg — same as get(), clearer intent (Java 10+):
try {
absent.orElseThrow();
} catch (NoSuchElementException e) {
System.out.println("orElseThrow() also throws NoSuchElementException");
}
// ── orElse() — EAGER evaluation of the fallback ───────────────────────
String defaultValue = computeExpensiveDefault(); // ALWAYS called, even if present!
String result1 = present.orElse(defaultValue);
System.out.println(result1); // "hello" — but computeExpensiveDefault() still ran!
static String computeExpensiveDefault() {
System.out.println("Computing expensive default..."); // proves eager evaluation
return "default";
}
// Demonstrating the eager evaluation problem:
Optional<String> alwaysPresent = Optional.of("value");
String wasted = alwaysPresent.orElse(computeExpensiveDefault());
// "Computing expensive default..." STILL prints, even though "value" is used
// ── orElseGet() — LAZY evaluation, correct for expensive defaults ─────
String result2 = present.orElseGet(() -> computeExpensiveDefault());
// computeExpensiveDefault() is NOT called because present has a value
System.out.println(result2); // "hello"
String result3 = absent.orElseGet(() -> computeExpensiveDefault());
// computeExpensiveDefault() IS called because absent has no value
System.out.println(result3); // "default" (with the print statement firing)
// ── orElseThrow(Supplier) — domain-specific exceptions ─────────────────
class UserNotFoundException extends RuntimeException {
UserNotFoundException(String id) { super("User not found: " + id); }
}
Optional<User> userOpt = findUserById("u123");
User user = userOpt.orElseThrow(() -> new UserNotFoundException("u123"));
// Throws UserNotFoundException with a clear message if absent,
// rather than the generic NoSuchElementException
// Common pattern: repository lookup with custom exception
User loadUser(String id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}Transformation — map, flatMap, filter, and Conditional Execution
// ── map() — transform present value, pass through empty ──────────────
Optional<String> name = Optional.of("alice");
Optional<String> upper = name.map(String::toUpperCase);
Optional<Integer> length = name.map(String::length);
System.out.println(upper); // Optional[ALICE]
System.out.println(length); // Optional[5]
Optional<String> empty = Optional.empty();
Optional<String> stillEmpty = empty.map(String::toUpperCase); // mapper never called
System.out.println(stillEmpty); // Optional.empty
// Chaining multiple map() calls:
Optional<Integer> result = Optional.of(" hello ")
.map(String::trim)
.map(String::toUpperCase)
.map(String::length);
System.out.println(result); // Optional[5]
// ── flatMap() — avoid nested Optional<Optional<U>> ────────────────────
record User(String id, String addressId) {}
record Address(String city) {}
Map<String, User> users = Map.of("u1", new User("u1", "a1"));
Map<String, Address> addresses = Map.of("a1", new Address("Springfield"));
Optional<User> findUser(String id) { return Optional.ofNullable(users.get(id)); }
Optional<Address> findAddress(String id) { return Optional.ofNullable(addresses.get(id)); }
// WRONG (conceptually) — map() would produce Optional<Optional<Address>>:
Optional<Optional<Address>> nested = findUser("u1").map(u -> findAddress(u.addressId()));
// CORRECT — flatMap() flattens to Optional<Address>:
Optional<Address> flat = findUser("u1").flatMap(u -> findAddress(u.addressId()));
System.out.println(flat); // Optional[Address[city=Springfield]]
Optional<String> city = findUser("u1")
.flatMap(u -> findAddress(u.addressId()))
.map(Address::city);
System.out.println(city); // Optional[Springfield]
// Chain that fails partway — short-circuits correctly:
Optional<String> missingCity = findUser("u999") // doesn't exist
.flatMap(u -> findAddress(u.addressId()))
.map(Address::city);
System.out.println(missingCity); // Optional.empty
// ── filter() — conditional presence ────────────────────────────────────
Optional<Integer> parsedAge = Optional.of(25);
Optional<Integer> validAge = parsedAge.filter(age -> age >= 18 && age <= 120);
System.out.println(validAge); // Optional[25]
Optional<Integer> invalidAge = Optional.of(-5).filter(age -> age >= 18);
System.out.println(invalidAge); // Optional.empty (failed the filter)
// Chained filter + map:
Optional<String> validatedName = Optional.of("Alice")
.filter(n -> !n.isBlank())
.filter(n -> n.length() <= 50)
.map(String::trim);
System.out.println(validatedName); // Optional[Alice]
// ── ifPresent / ifPresentOrElse ────────────────────────────────────────
Optional<String> maybeValue = Optional.of("data");
maybeValue.ifPresent(v -> System.out.println("Got: " + v)); // prints "Got: data"
Optional.<String>empty().ifPresent(v -> System.out.println("Never printed"));
// ifPresentOrElse — covers both branches (Java 9+):
maybeValue.ifPresentOrElse(
v -> System.out.println("Present: " + v),
() -> System.out.println("Was empty")
); // "Present: data"
Optional.<String>empty().ifPresentOrElse(
v -> System.out.println("Present: " + v),
() -> System.out.println("Was empty")
); // "Was empty"
// ── or() — fallback chains (Java 9+) ───────────────────────────────────
Optional<String> cacheResult = Optional.empty();
Optional<String> dbResult = Optional.of("from database");
Optional<String> finalResult = cacheResult
.or(() -> dbResult)
.or(() -> Optional.of("default fallback"));
System.out.println(finalResult); // Optional[from database]
// ── stream() — bridging Optional and Stream (Java 9+) ──────────────────
List<Optional<String>> optionals = List.of(
Optional.of("a"), Optional.empty(), Optional.of("b"), Optional.empty(), Optional.of("c")
);
// OLD idiom (pre-Java 9):
List<String> oldWay = optionals.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
// MODERN idiom (Java 9+):
List<String> modernWay = optionals.stream()
.flatMap(Optional::stream) // Optional → Stream of 0 or 1 elements, then flattened
.collect(Collectors.toList());
System.out.println(modernWay); // [a, b, c]Primitive Specializations and Established Usage Conventions
// ── OptionalInt, OptionalLong, OptionalDouble ──────────────────────────
IntStream numbers = IntStream.of(3, 7, 2, 9, 4);
OptionalInt max = numbers.max(); // no boxing
System.out.println(max.getAsInt()); // 9
System.out.println(max.isPresent()); // true
OptionalDouble avg = IntStream.of(1, 2, 3, 4, 5).average(); // always OptionalDouble
System.out.println(avg.getAsDouble()); // 3.0
OptionalLong sum = LongStream.of(1L, 2L, 3L).reduce(Long::sum);
System.out.println(sum.getAsLong()); // 6
// Empty stream produces empty Optional*:
OptionalInt emptyMax = IntStream.empty().max();
System.out.println(emptyMax.isPresent()); // false
System.out.println(emptyMax.orElse(-1)); // -1 (fallback)
// orElse / ifPresent work similarly to Optional<T>:
OptionalDouble.empty().ifPresent(d -> System.out.println("Won't print"));
double safeAvg = OptionalDouble.empty().orElse(0.0);
System.out.println(safeAvg); // 0.0
// ── CORRECT use: method return type for "may not exist" ───────────────
class UserRepository {
private final Map<String, User> users = new HashMap<>();
// CORRECT: Optional return type — absence is a normal, expected outcome
Optional<User> findById(String id) {
return Optional.ofNullable(users.get(id));
}
}
UserRepository repo = new UserRepository();
User found = repo.findById("u1")
.orElseThrow(() -> new NoSuchElementException("User not found"));
// ── ANTI-PATTERN 1: Optional as a field ────────────────────────────────
// WRONG:
class BadUser {
private Optional<String> middleName; // adds complexity, not Serializable
}
// CORRECT: just use a nullable field, documented:
class GoodUser {
/** May be null if the user has no middle name. */
private String middleName;
Optional<String> getMiddleName() { // Optional appears at the ACCESSOR boundary
return Optional.ofNullable(middleName);
}
}
// ── ANTI-PATTERN 2: Optional as a method parameter ─────────────────────
// WRONG — forces every caller to wrap arguments:
void createUserBad(String name, Optional<String> middleName) { /* ... */ }
createUserBad("Alice", Optional.of("Marie")); // awkward call site
createUserBad("Bob", Optional.empty()); // awkward call site
// CORRECT — use overloading:
void createUser(String name) { createUser(name, null); }
void createUser(String name, String middleName) { /* middleName may be null */ }
createUser("Alice", "Marie");
createUser("Bob"); // clean call sites
// ── ANTI-PATTERN 3: Optional inside collections ────────────────────────
// WRONG:
List<Optional<String>> badList = List.of(
Optional.of("a"), Optional.empty(), Optional.of("b")
);
// Just omit absent elements — a list naturally represents "zero or more":
// CORRECT:
List<String> goodList = List.of("a", "b"); // absent elements simply aren't there
// WRONG: Map<K, Optional<V>>:
Map<String, Optional<String>> badMap = new HashMap<>();
badMap.put("key1", Optional.of("value1"));
badMap.put("key2", Optional.empty()); // why even store this?
// CORRECT: absence of key already represents absence:
Map<String, String> goodMap = new HashMap<>();
goodMap.put("key1", "value1");
// key2 simply isn't present — checked via containsKey() or get() returning null
Optional<String> lookup = Optional.ofNullable(goodMap.get("key2")); // Optional appears here
// ── ANTI-PATTERN 4: Optional<Collection<T>> ────────────────────────────
// WRONG:
Optional<List<String>> badResults = Optional.of(searchResults);
if (badResults.isPresent() && !badResults.get().isEmpty()) { /* ... */ }
// CORRECT — empty list IS the "no results" signal:
List<String> goodResults = search(query); // returns empty list if nothing found
if (!goodResults.isEmpty()) { /* ... */ }
// ── Legitimate complex Optional chain ──────────────────────────────────
// This IS appropriate usage — Optional chaining at API boundaries:
String greeting = findUser(userId)
.flatMap(this::findPreferences)
.map(Preferences::language)
.map(lang -> "Hello in " + lang)
.orElse("Hello (default language)");