Optional Deep Dive — Practical Demo
Hands-on examples for Optional. Demonstrates safe value extraction, chaining with
map/flatMap, and the most common anti-patterns.
Understand Java's Functional Interfaces — Optional.map takes a Function, Optional.filter takes a Predicate, and Optional.ifPresent takes a Consumer.
Example 1: Creation and Safe Retrieval
All the ways to create an Optional and extract its value safely.
import java.util.Optional;
import java.util.NoSuchElementException;
public class OptionalCreation {
public static void main(String[] args) {
// --- Creation ---
Optional<String> present = Optional.of("Hello"); // ← must be non-null
Optional<String> empty = Optional.empty();
Optional<String> maybe = Optional.ofNullable(null); // ← null → empty
// BAD: Optional.of(null) throws NullPointerException immediately
// Optional<String> bad = Optional.of(null); // ← NPE
// --- Safe retrieval ---
// orElse — always evaluates the argument (even when present)
System.out.println(present.orElse("default")); // Hello
System.out.println(empty.orElse("default")); // default
// orElseGet — lazy: only evaluates Supplier when empty
String lazy = empty.orElseGet(() -> {
System.out.println(" Supplier called!");
return "lazy default";
});
System.out.println(lazy); // lazy default
// orElseThrow — throws custom exception when empty
try {
empty.orElseThrow(() -> new IllegalStateException("Value missing!"));
} catch (IllegalStateException e) {
System.out.println("Caught: " + e.getMessage()); // Caught: Value missing!
}
// ifPresent — only runs Consumer when a value is present
present.ifPresent(v -> System.out.println("Present: " + v)); // Present: Hello
empty.ifPresent(v -> System.out.println("Never printed")); // no-op
// ifPresentOrElse (Java 9+) — two-branch handling
empty.ifPresentOrElse(
v -> System.out.println("Value: " + v),
() -> System.out.println("No value found") // ← empty branch
);
}
}
Expected Output:
Hello
default
Supplier called!
lazy default
Caught: Value missing!
Present: Hello
No value found
orElse vs orElseGet is a common performance trap. orElse(expensiveCall()) ALWAYS calls expensiveCall(). Use orElseGet(() -> expensiveCall()) whenever the default computation is non-trivial.
Example 2: Chaining with map, flatMap, and filter
Eliminating nested null checks with Optional chains.
import java.util.Optional;
public class OptionalChaining {
record Address(String city, String zip, boolean verified) {}
record User(String name, Address address) {}
// Simulated repository returning Optional
static Optional<User> findUser(String email) {
if ("alice@example.com".equals(email))
return Optional.of(new User("Alice", new Address("Springfield", "12345", true)));
if ("ghost@example.com".equals(email))
return Optional.of(new User("Ghost", null)); // ← user exists but has no address
return Optional.empty(); // ← user not found
}
public static void main(String[] args) {
// OLD way — pyramid of null checks
String oldCity = null;
User user = findUser("alice@example.com").orElse(null);
if (user != null) {
Address addr = user.address();
if (addr != null) {
oldCity = addr.city();
}
}
System.out.println("Old way city: " + (oldCity != null ? oldCity : "Unknown"));
// OPTIONAL way — flat chain
String city = findUser("alice@example.com")
.map(User::address) // Optional<Address> — if user found
.map(Address::city) // Optional<String> — if address non-null
.orElse("Unknown");
System.out.println("Optional way city: " + city);
// Ghost user has null address — Optional.ofNullable wraps null safely
String ghostCity = findUser("ghost@example.com")
.map(User::address) // → Optional.ofNullable(null) = Optional.empty()
.map(Address::city)
.orElse("No city");
System.out.println("Ghost city: " + ghostCity);
// filter — keep only verified addresses
boolean isVerified = findUser("alice@example.com")
.map(User::address)
.filter(Address::verified) // ← Predicate: keep if verified
.isPresent();
System.out.println("Alice verified: " + isVerified);
// flatMap — when a method itself returns Optional
// (getPhoneNumber hypothetically returns Optional<String>)
// Instead, demonstrate flatMap with an explicit Optional-returning method
Optional<String> upperCity = findUser("alice@example.com")
.flatMap(u -> Optional.ofNullable(u.address()))
.map(Address::city)
.map(String::toUpperCase);
System.out.println("Upper city: " + upperCity.orElse("N/A"));
}
}
Expected Output:
Old way city: Springfield
Optional way city: Springfield
Ghost city: No city
Alice verified: true
Upper city: SPRINGFIELD
Example 3: Optional in a Spring-Style Service
A realistic pattern for service layer methods that return Optional.
import java.util.*;
import java.util.stream.*;
public class UserService {
record User(long id, String email, boolean active) {}
// In-memory store (simulates JPA repository)
private final Map<Long, User> store = Map.of(
1L, new User(1L, "alice@example.com", true),
2L, new User(2L, "bob@example.com", false),
3L, new User(3L, "carol@example.com", true)
);
// Repository layer — returns Optional
public Optional<User> findById(long id) {
return Optional.ofNullable(store.get(id)); // ← safe: ofNullable wraps null from map
}
// Service layer — uses Optional for domain logic
public String getEmailOrDefault(long id) {
return findById(id)
.filter(User::active) // ← only active users
.map(User::email) // ← extract email
.orElse("no-reply@example.com");// ← fallback
}
// orElseThrow — service throws domain exception
public User getOrThrow(long id) {
return findById(id)
.orElseThrow(() -> new NoSuchElementException("User not found: " + id));
}
public static void main(String[] args) {
UserService svc = new UserService();
System.out.println(svc.getEmailOrDefault(1L)); // alice@example.com (active)
System.out.println(svc.getEmailOrDefault(2L)); // no-reply: bob is inactive
System.out.println(svc.getEmailOrDefault(99L)); // no-reply: not found
try {
svc.getOrThrow(99L);
} catch (NoSuchElementException e) {
System.out.println("Exception: " + e.getMessage());
}
// Stream integration (Java 9+): flatMap Optional to filter missing users
List<Long> ids = List.of(1L, 2L, 99L, 3L);
List<String> activeEmails = ids.stream()
.map(svc::findById) // Stream<Optional<User>>
.flatMap(Optional::stream) // ← Java 9+: skip empty Optionals
.filter(User::active)
.map(User::email)
.collect(Collectors.toList());
System.out.println("Active emails: " + activeEmails);
}
}
Expected Output:
alice@example.com
no-reply@example.com
no-reply@example.com
Exception: User not found: 99
Active emails: [alice@example.com, carol@example.com]
Do NOT use Optional as a field type or method parameter. Optional<Long> userId as a record field or void process(Optional<String> name) as a parameter creates confusion (three states: null Optional, empty Optional, present Optional). Use Optional only as a return type to signal "this might not exist."
Exercises
Try these on your own to solidify understanding:
- Easy: Write a method
Optional<Integer> parseAge(String s)that returnsOptional.empty()if the string is null, blank, or not a valid integer, andOptional.of(parseInt)otherwise. Test with"25","abc", andnull. - Medium: Given
List<String> inputsthat may contain nulls, usestream(),map(Optional::ofNullable),flatMap(Optional::stream), andcollectto get a list with all nulls removed — without any explicit null checks. - Hard: Implement a method
<T, R> Optional<R> safeApply(Optional<T> input, Function<T, R> f)that returnsOptional.empty()if input is empty or iff.apply(t)throws any exception, and wraps the result inOptionalotherwise. Use this to safely parse arbitrary strings asInteger.
Back to Topic
Return to the Optional Deep Dive note for theory, interview questions, and further reading.