In 2009, Sir Tony Hoare — the computer scientist who invented the null reference in 1965 for the ALGOL W language — delivered a now-famous keynote titled “Null References: The Billion Dollar Mistake.” He said:
“I call it my billion-dollar mistake. It was the invention of the null reference in 1965… This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.”
That “billion dollars” is almost certainly an understatement. NullPointerException is the single most common runtime exception in Java. Stack Overflow data shows NPE questions outnumber every other Java exception combined. It crashes Android apps, takes down Spring Boot microservices at 3 AM, and hides in code that “works fine in dev” until the one edge case nobody tested reaches production.
The problem is not that “absence” needs to be represented. Every language needs a way to say “nothing here.” The problem is that in Java, null is a valid value for every reference type, and the compiler does nothing to force you to handle it. You can pass null anywhere, assign it to anything, and return it from any method. The NPE only appears at runtime, often far from where the null was introduced.
| Problem | Why It Hurts |
|---|---|
null is invisible in signatures |
User findByEmail(String email) — does it return null when not found? Throw an exception? You must read Javadoc or source code to find out. |
null propagates silently |
A null returned in one method gets passed through three more before it finally causes an NPE in a completely unrelated class. |
| Defensive checks pollute code | The only pre-Java 8 defense is scattering if (x != null) checks everywhere, obscuring actual business logic. |
| Bugs appear at runtime, not compile time | The compiler is happy. Your tests pass because they don’t cover that one path. Production breaks. |
public class NullProblemDemo {
public static void main(String[] args) {
// This compiles perfectly. No warnings. No errors.
String name = getUserName(42);
// But if getUserName returns null, this crashes at runtime
System.out.println(name.toUpperCase());
// Exception in thread "main" java.lang.NullPointerException
// The null traveled from getUserName() to here silently.
// With 50 methods between them, good luck finding the source.
}
// Nothing in this signature hints that null is possible
static String getUserName(int id) {
if (id == 1) return "Alice";
return null; // silent bomb -- compiles fine, blows up later
}
}
Before Optional, the standard defense was layered null checks. When objects are nested — User has an Address, Address has a City — the checks pile up into a deeply nested pyramid that buries the actual intent of the code.
// The "null check pyramid" -- real code from real projects
public String getCityName(User user) {
if (user != null) {
Address address = user.getAddress();
if (address != null) {
City city = address.getCity();
if (city != null) {
return city.getName();
}
}
}
return "Unknown";
}
// Three levels of nesting just to safely access a nested property.
// The business logic (get city name) is buried under defensive code.
Java was late to address this problem. Other languages had solutions long before Java 8:
| Language | Solution | Year |
|---|---|---|
| Haskell | Maybe monad |
1990 |
| Scala | Option[T] |
2004 |
| Groovy | Safe navigation operator ?. |
2007 |
| Kotlin | Nullable types with ? suffix |
2011 |
| Java | Optional<T> |
2014 (Java 8) |
| Swift | Optionals with ? and ! |
2014 |
Java’s Optional was directly inspired by Scala’s Option and Haskell’s Maybe. It was part of the larger Java 8 revolution that also brought lambdas and streams — and it was specifically designed to make the absence of a value explicit and safe.
Optional<T> is a container object introduced in Java 8 (package java.util) that may or may not hold a non-null value. Instead of returning null to mean “not found,” a method returns an Optional that explicitly says: “this result might be empty, and you must handle that case.”
Think of it like a gift box. When someone hands you a gift box, you know it might be empty. You would naturally open it and check before trying to use what is inside. Optional is that box — it wraps a value and reminds you to check before using it.
| Before Optional | With Optional |
|---|---|
Return null to mean “not found” |
Return Optional.empty() |
Check if (result != null) everywhere |
Use ifPresent(), orElse(), map() |
| Javadoc says “@return the user, or null” | Return type Optional<User> says it all |
| NullPointerException at runtime | Compiler-visible intent, handled at call site |
| Defensive null checks (nested ifs) | Functional chaining with map() and flatMap() |
This is critical to understand from the start. Optional is not a replacement for all nulls. It was specifically designed for one purpose: method return types where absence is a valid outcome.
Brian Goetz, Java Language Architect at Oracle, said it clearly:
“Optional is intended to provide a limited mechanism for library method return types where there is a clear need to represent ‘no result,’ and where using null for that is overwhelmingly likely to cause errors.”
import java.util.Optional;
public class OptionalBasicDemo {
public static void main(String[] args) {
// Before: returns null, caller must "just know" to check
String resultOld = findNicknameOld("Bob");
// resultOld.toUpperCase(); // NPE if Bob has no nickname!
// After: return type makes absence explicit
Optional resultNew = findNickname("Bob");
// resultNew.get().toUpperCase(); // You're reminded: it might be empty!
// The safe way:
String nickname = resultNew.orElse("No nickname");
System.out.println(nickname);
// Output: No nickname
}
// OLD: returns null -- no indication in the signature
static String findNicknameOld(String name) {
if (name.equals("Alice")) return "Ally";
return null;
}
// NEW: Optional makes absence explicit in the signature
static Optional findNickname(String name) {
if (name.equals("Alice")) return Optional.of("Ally");
return Optional.empty();
}
}
There are exactly three ways to create an Optional. Each has a specific purpose, and using the wrong one is a common source of bugs.
Use Optional.of() when you are absolutely certain the value is not null. If you pass null, it throws NullPointerException immediately. This is intentional — it fails fast rather than hiding a bug.
import java.util.Optional;
public class OptionalOfDemo {
public static void main(String[] args) {
// Use Optional.of() when you KNOW the value exists
Optional name = Optional.of("Alice");
System.out.println(name);
// Output: Optional[Alice]
// If you accidentally pass null, you get NPE immediately
// This is GOOD -- it fails fast at the source
try {
Optional bad = Optional.of(null);
} catch (NullPointerException e) {
System.out.println("Optional.of(null) throws NPE: " + e.getMessage());
}
// Output: Optional.of(null) throws NPE: null
// Real-world use: wrapping a value you just created or validated
String username = "admin";
if (username != null && !username.isEmpty()) {
Optional validUser = Optional.of(username); // safe: already checked
System.out.println("Valid user: " + validUser.get());
}
// Output: Valid user: admin
}
}
Use Optional.ofNullable() when the value might be null. If the value is non-null, it wraps it. If the value is null, it returns Optional.empty(). This is the most commonly used factory method.
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class OptionalOfNullableDemo {
public static void main(String[] args) {
// Map.get() returns null if key is not found
Map config = new HashMap<>();
config.put("timeout", "30");
// Wrap a possibly-null value
Optional timeout = Optional.ofNullable(config.get("timeout"));
Optional retries = Optional.ofNullable(config.get("retries"));
System.out.println("timeout: " + timeout);
// Output: timeout: Optional[30]
System.out.println("retries: " + retries);
// Output: retries: Optional.empty
// Safely use the values
System.out.println("Timeout value: " + timeout.orElse("10"));
// Output: Timeout value: 30
System.out.println("Retries value: " + retries.orElse("3"));
// Output: Retries value: 3
// Common pattern: wrap external/legacy API results
String systemProp = System.getProperty("app.mode"); // might be null
String mode = Optional.ofNullable(systemProp).orElse("development");
System.out.println("Mode: " + mode);
// Output: Mode: development
}
}
Use Optional.empty() to explicitly return “nothing.” This replaces return null in methods that return Optional.
import java.util.Optional;
import java.util.List;
public class OptionalEmptyDemo {
public static void main(String[] args) {
// Use Optional.empty() when a search/lookup finds nothing
Optional found = findUserById(1);
Optional notFound = findUserById(99);
System.out.println("ID 1: " + found);
// Output: ID 1: Optional[Alice]
System.out.println("ID 99: " + notFound);
// Output: ID 99: Optional.empty
// Pattern: conditional return
Optional result = isFeatureEnabled("dark-mode")
? Optional.of("enabled")
: Optional.empty();
System.out.println("Feature: " + result);
// Output: Feature: Optional.empty
}
static Optional findUserById(int id) {
List users = List.of("Alice", "Bob", "Charlie");
if (id >= 0 && id < users.size()) {
return Optional.of(users.get(id));
}
return Optional.empty(); // NOT return null!
}
static boolean isFeatureEnabled(String feature) {
return false; // simplified
}
}
| Method | When to Use | If Null Passed |
|---|---|---|
Optional.of(value) |
Value is guaranteed non-null (you just created it, validated it, or it comes from a non-null source) | Throws NullPointerException |
Optional.ofNullable(value) |
Value might be null (from a Map lookup, legacy API, database query, external input) | Returns Optional.empty() |
Optional.empty() |
You know there is no value (failed search, unmet condition, default case in switch) | N/A -- no argument |
Rule of thumb: If you are wrapping a value that came from somewhere else and you are not 100% sure it is non-null, use ofNullable(). If you just created the value yourself, use of(). If there is nothing to return, use empty().
isPresent() returns true if the Optional contains a value. isEmpty() (added in Java 11) is the logical opposite -- returns true if the Optional is empty. Both are simple boolean checks.
import java.util.Optional;
public class IsPresentDemo {
public static void main(String[] args) {
Optional full = Optional.of("Hello");
Optional empty = Optional.empty();
// isPresent() -- available since Java 8
System.out.println("full.isPresent(): " + full.isPresent());
// Output: full.isPresent(): true
System.out.println("empty.isPresent(): " + empty.isPresent());
// Output: empty.isPresent(): false
// isEmpty() -- available since Java 11
System.out.println("full.isEmpty(): " + full.isEmpty());
// Output: full.isEmpty(): false
System.out.println("empty.isEmpty(): " + empty.isEmpty());
// Output: empty.isEmpty(): true
// Conditional logic with isPresent
Optional username = findUsername("admin@test.com");
if (username.isPresent()) {
System.out.println("Found: " + username.get());
} else {
System.out.println("User not found");
}
// Output: Found: admin
}
static Optional findUsername(String email) {
if (email.contains("admin")) return Optional.of("admin");
return Optional.empty();
}
}
get() returns the contained value if present, or throws NoSuchElementException if empty. It is almost always wrong to call get() directly. It defeats the entire purpose of Optional by reintroducing the same problem as null -- an unchecked exception at runtime.
The Java API designers have acknowledged this was a design mistake. In fact, get() has been deprecated-for-replacement in more recent JDK builds, with orElseThrow() recommended as the explicit alternative when you truly want to throw if empty.
import java.util.NoSuchElementException;
import java.util.Optional;
public class GetAntiPatternDemo {
public static void main(String[] args) {
Optional empty = Optional.empty();
// BAD: get() on empty Optional throws NoSuchElementException
try {
String value = empty.get();
} catch (NoSuchElementException e) {
System.out.println("get() on empty: " + e.getClass().getSimpleName());
}
// Output: get() on empty: NoSuchElementException
// BAD: isPresent() + get() -- just a null check with extra steps
Optional name = Optional.of("Alice");
if (name.isPresent()) {
System.out.println(name.get()); // Works, but misses the point
}
// This is the SAME pattern as: if (name != null) { use(name); }
// You replaced a null check with an isPresent check. No improvement.
// GOOD alternatives (covered in detail in the next sections):
System.out.println(name.orElse("Unknown")); // default value
name.ifPresent(n -> System.out.println(n)); // consume if present
String upper = name.map(String::toUpperCase) // transform
.orElse("UNKNOWN");
System.out.println(upper);
// Output: Alice
// Output: Alice
// Output: ALICE
}
}
| Instead of | Use | Why |
|---|---|---|
opt.get() |
opt.orElse(default) |
Provides a safe fallback |
opt.get() |
opt.orElseThrow() |
Throws explicitly -- your intent is clear |
if (opt.isPresent()) opt.get() |
opt.ifPresent(value -> ...) |
Cleaner, no redundant check |
if (opt.isPresent()) return opt.get().getX() |
opt.map(v -> v.getX()) |
Functional transformation |
When an Optional is empty, you usually want a fallback value. Java provides three methods for this, and understanding the difference between them -- especially eager vs lazy evaluation -- is critical.
orElse() returns the contained value if present, or the provided default value if empty. The default value is always evaluated, even when the Optional is not empty.
import java.util.Optional;
public class OrElseDemo {
public static void main(String[] args) {
Optional present = Optional.of("Alice");
Optional empty = Optional.empty();
// orElse with a present Optional -- returns the contained value
String name1 = present.orElse("Default");
System.out.println(name1);
// Output: Alice
// orElse with an empty Optional -- returns the default
String name2 = empty.orElse("Default");
System.out.println(name2);
// Output: Default
// Great for configuration defaults
String port = Optional.ofNullable(System.getenv("PORT")).orElse("8080");
System.out.println("Server port: " + port);
// Output: Server port: 8080
// Great for display values
String displayName = Optional.ofNullable(getUserNickname())
.orElse("Anonymous");
System.out.println("Display: " + displayName);
// Output: Display: Anonymous
}
static String getUserNickname() {
return null; // user has no nickname
}
}
orElseGet() takes a Supplier<T> that is only called when the Optional is empty. This is the lazy alternative to orElse(). Use it when the default value is expensive to compute (database query, network call, complex calculation).
import java.util.Optional;
public class OrElseGetDemo {
public static void main(String[] args) {
Optional present = Optional.of("Alice");
Optional empty = Optional.empty();
// orElseGet with a present Optional -- supplier is NEVER called
String name1 = present.orElseGet(() -> {
System.out.println("Computing default...");
return "Default";
});
System.out.println(name1);
// Output: Alice
// Note: "Computing default..." was NOT printed
// orElseGet with an empty Optional -- supplier IS called
String name2 = empty.orElseGet(() -> {
System.out.println("Computing default...");
return "Default";
});
// Output: Computing default...
System.out.println(name2);
// Output: Default
// Real-world: expensive fallback
Optional cachedConfig = Optional.empty();
String config = cachedConfig.orElseGet(() -> loadConfigFromDatabase());
System.out.println("Config: " + config);
// Output: Config: default-config-from-db
}
static String loadConfigFromDatabase() {
// Simulate expensive DB query
System.out.println(" (querying database...)");
return "default-config-from-db";
}
}
This is the most commonly misunderstood aspect of Optional. orElse() evaluates its argument eagerly -- the default is computed regardless of whether the Optional is empty or not. This can cause bugs when the default has side effects.
import java.util.Optional;
public class EagerVsLazyDemo {
public static void main(String[] args) {
System.out.println("===== orElse() -- EAGER evaluation =====");
Optional present = Optional.of("Alice");
// Even though Optional has a value, the fallback method STILL runs
String result1 = present.orElse(expensiveDefault());
System.out.println("Result: " + result1);
// Output:
// Expensive operation executed!
// Result: Alice
// The expensive operation ran UNNECESSARILY.
System.out.println();
System.out.println("===== orElseGet() -- LAZY evaluation =====");
// With orElseGet, the supplier is NOT called when value is present
String result2 = present.orElseGet(() -> expensiveDefault());
System.out.println("Result: " + result2);
// Output:
// Result: Alice
// The expensive operation did NOT run. Much better.
System.out.println();
System.out.println("===== Both with empty Optional =====");
Optional empty = Optional.empty();
String r1 = empty.orElse(expensiveDefault());
System.out.println("orElse: " + r1);
// Output:
// Expensive operation executed!
// orElse: fallback-value
String r2 = empty.orElseGet(() -> expensiveDefault());
System.out.println("orElseGet: " + r2);
// Output:
// Expensive operation executed!
// orElseGet: fallback-value
// When empty, both execute the default. The difference only matters
// when the Optional HAS a value.
}
static String expensiveDefault() {
System.out.println(" Expensive operation executed!");
return "fallback-value";
}
}
| Aspect | orElse(value) |
orElseGet(supplier) |
|---|---|---|
| Evaluation | Eager -- always computed | Lazy -- only when empty |
| Use when default is | A simple constant or already-computed value | Expensive (DB, network, calculation) |
| Side effects | Dangerous -- runs even when not needed | Safe -- only runs when needed |
| Performance | Wasteful if default is expensive and Optional is present | Optimal -- no unnecessary computation |
| Example | .orElse("N/A") |
.orElseGet(() -> queryDB()) |
Sometimes an empty Optional means something went wrong. orElseThrow() lets you throw a specific exception. Java 10 added a no-arg version that throws NoSuchElementException.
import java.util.Optional;
public class OrElseThrowDemo {
public static void main(String[] args) {
Optional user = findUser("admin@test.com");
// orElseThrow with custom exception (Java 8+)
String name = user.orElseThrow(
() -> new IllegalArgumentException("User not found: admin@test.com")
);
System.out.println("Found: " + name);
// Output: Found: admin
// orElseThrow no-arg version (Java 10+)
// Throws NoSuchElementException with no message
Optional empty = Optional.empty();
try {
empty.orElseThrow(); // Java 10+
} catch (Exception e) {
System.out.println("No-arg: " + e.getClass().getSimpleName());
}
// Output: No-arg: NoSuchElementException
// Common pattern: service layer validation
// User currentUser = userRepository.findById(userId)
// .orElseThrow(() -> new UserNotFoundException(userId));
}
static Optional findUser(String email) {
if (email.contains("admin")) return Optional.of("admin");
return Optional.empty();
}
}
or() was added in Java 9. When the current Optional is empty, it returns the Optional produced by the supplier. This allows chaining fallback sources.
import java.util.Optional;
public class OrMethodDemo {
public static void main(String[] args) {
// or() chains multiple Optional sources (Java 9+)
// Try each source in order until one has a value
String result = findInCache("theme")
.or(() -> findInDatabase("theme"))
.or(() -> findInDefaults("theme"))
.orElse("system-default");
System.out.println("Theme: " + result);
// Output:
// Cache miss for: theme
// DB miss for: theme
// Default found for: theme
// Theme: dark-mode
// When the first source has a value, later sources are skipped
String result2 = findInCache("language")
.or(() -> findInDatabase("language"))
.or(() -> findInDefaults("language"))
.orElse("en");
System.out.println("Language: " + result2);
// Output:
// Cache hit for: language
// Language: en-US
}
static Optional findInCache(String key) {
if (key.equals("language")) {
System.out.println(" Cache hit for: " + key);
return Optional.of("en-US");
}
System.out.println(" Cache miss for: " + key);
return Optional.empty();
}
static Optional findInDatabase(String key) {
System.out.println(" DB miss for: " + key);
return Optional.empty();
}
static Optional findInDefaults(String key) {
if (key.equals("theme")) {
System.out.println(" Default found for: " + key);
return Optional.of("dark-mode");
}
return Optional.empty();
}
}
Optional's real power comes from its functional transformation methods: map(), flatMap(), and filter(). These let you chain operations on the contained value without ever unwrapping the Optional or writing if statements.
map() takes a function, applies it to the contained value (if present), and wraps the result in a new Optional. If the original Optional is empty, map() returns Optional.empty() without calling the function.
import java.util.Optional;
public class MapDemo {
public static void main(String[] args) {
Optional name = Optional.of("alice");
Optional empty = Optional.empty();
// map transforms the value inside the Optional
Optional upper = name.map(String::toUpperCase);
System.out.println(upper);
// Output: Optional[ALICE]
// map on empty returns empty -- function is never called
Optional upperEmpty = empty.map(String::toUpperCase);
System.out.println(upperEmpty);
// Output: Optional.empty
// Chaining multiple maps
Optional length = name
.map(String::trim)
.map(String::toUpperCase)
.map(String::length);
System.out.println("Length: " + length.orElse(0));
// Output: Length: 5
// map with method that returns a different type
Optional email = Optional.of("alice@example.com");
Optional domain = email.map(e -> e.substring(e.indexOf('@') + 1));
System.out.println("Domain: " + domain.orElse("unknown"));
// Output: Domain: example.com
// Before Optional (ugly null chain):
// String domain = null;
// if (email != null) {
// domain = email.substring(email.indexOf('@') + 1);
// }
}
}
When your transformation function itself returns an Optional, using map() would produce a nested Optional<Optional<T>>. flatMap() solves this by "flattening" the result into a single Optional<T>.
This is essential when chaining methods that return Optional -- for example, navigating a chain of objects where each getter might return Optional.
import java.util.Optional;
public class FlatMapDemo {
public static void main(String[] args) {
// Setup: User -> Address -> ZipCode, each getter returns Optional
User user = new User("Alice",
new Address("123 Main St",
new ZipCode("90210")));
User userNoAddress = new User("Bob", null);
// PROBLEM with map(): nested Optionals
Optional>> nested = Optional.of(user)
.map(User::getAddress) // Optional>
.map(opt -> opt.map(Address::getZipCode)); // gets ugly fast
// This is unworkable.
// SOLUTION with flatMap(): flat chain
String zip1 = Optional.of(user)
.flatMap(User::getAddress) // Optional
.flatMap(Address::getZipCode) // Optional
.map(ZipCode::getCode) // Optional
.orElse("N/A");
System.out.println("Alice's zip: " + zip1);
// Output: Alice's zip: 90210
// Same chain with a user who has no address
String zip2 = Optional.of(userNoAddress)
.flatMap(User::getAddress) // Optional.empty
.flatMap(Address::getZipCode) // skipped
.map(ZipCode::getCode) // skipped
.orElse("N/A");
System.out.println("Bob's zip: " + zip2);
// Output: Bob's zip: N/A
// No NPE, no null checks, clean chain.
}
}
class User {
private String name;
private Address address;
User(String name, Address address) {
this.name = name;
this.address = address;
}
Optional getAddress() {
return Optional.ofNullable(address);
}
String getName() { return name; }
}
class Address {
private String street;
private ZipCode zipCode;
Address(String street, ZipCode zipCode) {
this.street = street;
this.zipCode = zipCode;
}
Optional getZipCode() {
return Optional.ofNullable(zipCode);
}
String getStreet() { return street; }
}
class ZipCode {
private String code;
ZipCode(String code) { this.code = code; }
String getCode() { return code; }
}
| Use | When | Function Returns | Result |
|---|---|---|---|
map(f) |
Transformation returns a plain value | T -> R |
Optional<R> |
flatMap(f) |
Transformation returns an Optional | T -> Optional<R> |
Optional<R> |
If the function returns Optional, use flatMap(). If it returns anything else, use map().
filter() checks if the contained value matches a predicate. If it does, the Optional is returned as-is. If it does not, an empty Optional is returned. If the Optional is already empty, filter() returns empty without calling the predicate.
import java.util.Optional;
public class FilterDemo {
public static void main(String[] args) {
Optional name = Optional.of("Alice");
// filter keeps the value only if it matches the predicate
Optional startsWithA = name.filter(n -> n.startsWith("A"));
System.out.println(startsWithA);
// Output: Optional[Alice]
Optional startsWithB = name.filter(n -> n.startsWith("B"));
System.out.println(startsWithB);
// Output: Optional.empty
// Practical: validate input
Optional password = Optional.of("Str0ngP@ss!");
boolean isValid = password
.filter(p -> p.length() >= 8)
.filter(p -> p.matches(".*[A-Z].*"))
.filter(p -> p.matches(".*[0-9].*"))
.filter(p -> p.matches(".*[!@#$%^&*].*"))
.isPresent();
System.out.println("Password valid: " + isValid);
// Output: Password valid: true
// Chaining filter with map
Optional age = Optional.of(25);
String category = age
.filter(a -> a >= 18)
.map(a -> "Adult")
.orElse("Minor");
System.out.println("Category: " + category);
// Output: Category: Adult
// filter on empty Optional -- predicate never called
Optional empty = Optional.empty();
Optional filtered = empty.filter(s -> {
System.out.println("This never prints");
return true;
});
System.out.println(filtered);
// Output: Optional.empty
}
}
The real elegance of Optional appears when you chain map(), flatMap(), and filter() together. Compare the before and after:
import java.util.Optional;
import java.util.Map;
import java.util.HashMap;
public class ChainingDemo {
public static void main(String[] args) {
Map userEmails = new HashMap<>();
userEmails.put("alice", "Alice.Smith@Company.COM");
userEmails.put("bob", "");
// BEFORE Optional: nested null checks + validation
// String email = userEmails.get("alice");
// String domain = null;
// if (email != null) {
// email = email.trim();
// if (!email.isEmpty()) {
// email = email.toLowerCase();
// int at = email.indexOf('@');
// if (at >= 0) {
// domain = email.substring(at + 1);
// }
// }
// }
// if (domain == null) domain = "unknown";
// AFTER Optional: clean chain
String domain = Optional.ofNullable(userEmails.get("alice"))
.map(String::trim)
.filter(e -> !e.isEmpty())
.map(String::toLowerCase)
.filter(e -> e.contains("@"))
.map(e -> e.substring(e.indexOf('@') + 1))
.orElse("unknown");
System.out.println("Alice domain: " + domain);
// Output: Alice domain: company.com
// Same chain with empty email
String domain2 = Optional.ofNullable(userEmails.get("bob"))
.map(String::trim)
.filter(e -> !e.isEmpty()) // "" fails here -> empty
.map(String::toLowerCase) // skipped
.filter(e -> e.contains("@")) // skipped
.map(e -> e.substring(e.indexOf('@') + 1)) // skipped
.orElse("unknown");
System.out.println("Bob domain: " + domain2);
// Output: Bob domain: unknown
// Same chain with unknown user
String domain3 = Optional.ofNullable(userEmails.get("charlie"))
.map(String::trim) // null from map -> empty
.filter(e -> !e.isEmpty()) // skipped
.map(String::toLowerCase) // skipped
.filter(e -> e.contains("@")) // skipped
.map(e -> e.substring(e.indexOf('@') + 1)) // skipped
.orElse("unknown");
System.out.println("Charlie domain: " + domain3);
// Output: Charlie domain: unknown
}
}
ifPresent() accepts a Consumer<T> that is called only when the Optional contains a value. If the Optional is empty, nothing happens. This is the ideal replacement for the if (x != null) { use(x); } pattern.
import java.util.Optional;
public class IfPresentDemo {
public static void main(String[] args) {
Optional present = Optional.of("Alice");
Optional empty = Optional.empty();
// ifPresent with lambda
present.ifPresent(name -> System.out.println("Hello, " + name + "!"));
// Output: Hello, Alice!
// Nothing happens for empty Optional
empty.ifPresent(name -> System.out.println("This never prints"));
// (no output)
// ifPresent with method reference
Optional email = Optional.of("alice@test.com");
email.ifPresent(System.out::println);
// Output: alice@test.com
// Practical: save only if value exists
Optional userInput = getUserInput();
userInput.ifPresent(input -> {
System.out.println("Saving: " + input);
// saveToDatabase(input);
});
// Output: Saving: user-provided-value
}
static Optional getUserInput() {
return Optional.of("user-provided-value");
}
}
ifPresentOrElse() was added in Java 9. It handles both cases: if the value is present, the consumer runs; if empty, the empty-action runnable runs. This replaces the if/else pattern completely.
import java.util.Optional;
public class IfPresentOrElseDemo {
public static void main(String[] args) {
Optional present = Optional.of("Alice");
Optional empty = Optional.empty();
// Java 9+: handle both present and empty cases
present.ifPresentOrElse(
name -> System.out.println("Welcome back, " + name + "!"),
() -> System.out.println("Welcome, guest!")
);
// Output: Welcome back, Alice!
empty.ifPresentOrElse(
name -> System.out.println("Welcome back, " + name + "!"),
() -> System.out.println("Welcome, guest!")
);
// Output: Welcome, guest!
// Practical: logging
Optional config = loadConfig("database.url");
config.ifPresentOrElse(
url -> System.out.println("Using database: " + url),
() -> System.out.println("WARNING: No database URL configured, using default")
);
// Output: WARNING: No database URL configured, using default
// Before Java 9, you had to write:
// if (config.isPresent()) {
// System.out.println("Using: " + config.get());
// } else {
// System.out.println("WARNING: not configured");
// }
}
static Optional loadConfig(String key) {
return Optional.empty();
}
}
Optional and Streams were both introduced in Java 8 and are designed to work together. Java 9 strengthened this integration with Optional.stream().
stream() converts an Optional into a Stream: a one-element stream if present, an empty stream if not. This is extremely useful when you have a collection of Optionals and want to extract only the present values.
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class OptionalStreamDemo {
public static void main(String[] args) {
// Optional.stream() -- Java 9+
Optional present = Optional.of("Alice");
Optional empty = Optional.empty();
present.stream().forEach(System.out::println);
// Output: Alice
empty.stream().forEach(System.out::println);
// (no output -- empty stream)
// Real power: filtering a list of Optionals
List> results = List.of(
Optional.of("Alice"),
Optional.empty(),
Optional.of("Bob"),
Optional.empty(),
Optional.of("Charlie")
);
// Java 9+: use Optional.stream() with flatMap
List names = results.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
System.out.println("Names: " + names);
// Output: Names: [Alice, Bob, Charlie]
// Before Java 9, you had to write:
// List names = results.stream()
// .filter(Optional::isPresent)
// .map(Optional::get)
// .collect(Collectors.toList());
}
}
Several Stream terminal operations return Optionals: findFirst(), findAny(), min(), max(), and reduce(). These are natural places to use Optional's functional methods.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class StreamOptionalDemo {
public static void main(String[] args) {
List names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List empty = List.of();
// findFirst() returns Optional
Optional first = names.stream()
.filter(n -> n.startsWith("C"))
.findFirst();
System.out.println("First C-name: " + first.orElse("none"));
// Output: First C-name: Charlie
// min() returns Optional
Optional shortest = names.stream()
.min(Comparator.comparingInt(String::length));
shortest.ifPresent(s -> System.out.println("Shortest: " + s));
// Output: Shortest: Bob
// reduce() returns Optional (when no identity)
Optional sum = names.stream()
.map(String::length)
.reduce(Integer::sum);
System.out.println("Total chars: " + sum.orElse(0));
// Output: Total chars: 20
// findFirst on empty stream returns Optional.empty
Optional fromEmpty = empty.stream()
.findFirst();
System.out.println("From empty: " + fromEmpty);
// Output: From empty: Optional.empty
// Chain stream result with Optional operations
String result = names.stream()
.filter(n -> n.length() > 10)
.findFirst()
.map(String::toUpperCase)
.orElse("No long names found");
System.out.println(result);
// Output: No long names found
}
}
A common real-world scenario: you have a list of objects, each with a method that returns Optional. You want to collect only the present values into a flat list.
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class StreamFlatMapOptionalDemo {
public static void main(String[] args) {
List employees = List.of(
new Employee("Alice", "Engineering"),
new Employee("Bob", null), // no department
new Employee("Charlie", "Marketing"),
new Employee("David", null), // no department
new Employee("Eve", "Engineering")
);
// Collect only assigned departments (unique)
List departments = employees.stream()
.map(Employee::getDepartment) // Stream>
.flatMap(Optional::stream) // Stream -- empties removed
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println("Active departments: " + departments);
// Output: Active departments: [Engineering, Marketing]
// Count employees with departments
long assigned = employees.stream()
.map(Employee::getDepartment)
.filter(Optional::isPresent)
.count();
System.out.println("Employees with departments: " + assigned + "/" + employees.size());
// Output: Employees with departments: 3/5
}
}
class Employee {
private String name;
private String department;
Employee(String name, String department) {
this.name = name;
this.department = department;
}
String getName() { return name; }
Optional getDepartment() {
return Optional.ofNullable(department);
}
}
The most important aspect of Optional is not how to use its methods, but where to use Optional and where not to. This section covers the API design rules that the Java team intended.
This is the primary and intended use of Optional: when a method might legitimately return no value.
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class ReturnTypeDemo {
private static Map users = new HashMap<>(Map.of(
1, "Alice",
2, "Bob"
));
// GOOD: return Optional when "not found" is a normal outcome
public static Optional findById(int id) {
return Optional.ofNullable(users.get(id));
}
// GOOD: search that may find nothing
public static Optional findByNamePrefix(String prefix) {
return users.values().stream()
.filter(name -> name.startsWith(prefix))
.findFirst();
}
// BAD: do NOT return Optional when there is always a result
// public static Optional> getAllUsers() -- WRONG
// Return an empty list instead!
// public static List getAllUsers() -- CORRECT
public static void main(String[] args) {
System.out.println(findById(1).orElse("Not found"));
// Output: Alice
System.out.println(findById(99).orElse("Not found"));
// Output: Not found
findByNamePrefix("A").ifPresent(n -> System.out.println("Found: " + n));
// Output: Found: Alice
}
}
Using Optional as a method parameter is an anti-pattern. It forces callers to wrap their values, adds complexity, and the caller already knows whether they have a value or not. Use method overloading instead.
import java.util.Optional;
public class ParameterAntiPatternDemo {
// BAD: Optional as parameter
// Forces callers to write: sendEmail(Optional.of("Custom Subject"))
// or sendEmail(Optional.empty())
// Callers already know if they have a subject or not!
static void sendEmailBad(String to, Optional subject) {
String subj = subject.orElse("No Subject");
System.out.println("To: " + to + ", Subject: " + subj);
}
// GOOD: method overloading
static void sendEmail(String to) {
sendEmail(to, "No Subject");
}
static void sendEmail(String to, String subject) {
System.out.println("To: " + to + ", Subject: " + subject);
}
public static void main(String[] args) {
// BAD: caller is forced to wrap
sendEmailBad("alice@test.com", Optional.of("Hello"));
sendEmailBad("bob@test.com", Optional.empty());
// GOOD: clean calls
sendEmail("alice@test.com", "Hello");
sendEmail("bob@test.com");
// Output:
// To: alice@test.com, Subject: Hello
// To: bob@test.com, Subject: No Subject
// To: alice@test.com, Subject: Hello
// To: bob@test.com, Subject: No Subject
}
}
Optional was intentionally designed to not implement Serializable. This was a deliberate choice by the Java team to discourage its use as a field type. Using Optional as a field wastes memory (extra object allocation), breaks serialization frameworks (Jackson, JPA, etc.), and is not what the API was designed for.
import java.util.Optional;
public class FieldAntiPatternDemo {
// BAD: Optional as a field
// - Not Serializable
// - Extra memory overhead (wraps every value in an object)
// - Breaks JPA, Jackson, and most serialization frameworks
static class UserBad {
private String name;
private Optional nickname; // DON'T DO THIS
UserBad(String name, String nickname) {
this.name = name;
this.nickname = Optional.ofNullable(nickname);
}
}
// GOOD: nullable field with Optional getter
// The field is a plain nullable type. The Optional is used only
// in the return type of the getter, which is its intended purpose.
static class UserGood {
private String name;
private String nickname; // nullable -- that's fine
UserGood(String name, String nickname) {
this.name = name;
this.nickname = nickname;
}
public String getName() { return name; }
// Return Optional from getter -- this IS the intended use
public Optional getNickname() {
return Optional.ofNullable(nickname);
}
}
public static void main(String[] args) {
UserGood user = new UserGood("Alice", null);
user.getNickname().ifPresentOrElse(
nick -> System.out.println("Nickname: " + nick),
() -> System.out.println("No nickname set")
);
// Output: No nickname set
}
}
| Context | Use Optional? | Why |
|---|---|---|
| Method return type (value may be absent) | Yes | This is its designed purpose |
| Method parameter | No | Use overloading or @Nullable |
| Class field / instance variable | No | Not Serializable, memory overhead |
| Constructor parameter | No | Same as method parameter |
| Collection element | No | Use filter to remove nulls instead |
| Map key or value | No | Use Map.getOrDefault() or computeIfAbsent() |
| Return type with collections | No | Return empty collection, not Optional<List> |
| Getter for potentially absent data | Yes | Return Optional from getter, store field as nullable |
Optional is one of the most misused features in Java. Here are eight anti-patterns that experienced developers still fall into.
This is the #1 Optional anti-pattern. It is just a null check with extra steps and completely defeats the purpose of Optional.
import java.util.Optional;
public class AntiPattern1Demo {
public static void main(String[] args) {
Optional name = Optional.of("Alice");
// BAD: isPresent + get = null check with extra steps
if (name.isPresent()) {
System.out.println("Hello, " + name.get());
} else {
System.out.println("Hello, stranger");
}
// This is identical to:
// if (name != null) { use(name); } else { useDefault(); }
// You gained NOTHING from Optional.
// GOOD: use functional methods
// Option A: ifPresentOrElse (Java 9+)
name.ifPresentOrElse(
n -> System.out.println("Hello, " + n),
() -> System.out.println("Hello, stranger")
);
// Option B: map + orElse
String greeting = name.map(n -> "Hello, " + n)
.orElse("Hello, stranger");
System.out.println(greeting);
// Output:
// Hello, Alice
// Hello, Alice
// Hello, Alice
}
}
Calling Optional.of(null) throws NullPointerException. If the value might be null, always use ofNullable().
import java.util.Optional;
import java.util.Map;
public class AntiPattern2Demo {
public static void main(String[] args) {
Map config = Map.of("host", "localhost");
// BAD: Map.get() can return null -- this throws NPE!
try {
Optional port = Optional.of(config.get("port")); // NPE!
} catch (NullPointerException e) {
System.out.println("NPE from Optional.of(null)!");
}
// Output: NPE from Optional.of(null)!
// GOOD: use ofNullable when value might be null
Optional port = Optional.ofNullable(config.get("port"));
System.out.println("Port: " + port.orElse("8080"));
// Output: Port: 8080
}
}
Already covered in section 9.3. Optional is not Serializable, wastes memory, and breaks most frameworks.
Already covered in section 9.2. Use method overloading or @Nullable annotations instead.
Never store Optionals in a List, Set, or Map. Filter out nulls instead.
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public class AntiPattern5Demo {
public static void main(String[] args) {
// BAD: List of Optionals
List> bad = new ArrayList<>();
bad.add(Optional.of("Alice"));
bad.add(Optional.empty());
bad.add(Optional.of("Bob"));
// Now you have to unwrap every element when using the list.
// GOOD: filter out nulls, keep a clean list
List rawData = List.of("Alice", "", "Bob", "", "Charlie");
List good = rawData.stream()
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
System.out.println("Clean list: " + good);
// Output: Clean list: [Alice, Bob, Charlie]
// GOOD: filter nulls from a list that may contain null
List withNulls = new ArrayList<>();
withNulls.add("Alice");
withNulls.add(null);
withNulls.add("Bob");
List filtered = withNulls.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
System.out.println("Filtered: " + filtered);
// Output: Filtered: [Alice, Bob]
}
}
Some developers, after learning about Optional, try to use it for every single variable. Optional has a purpose: method return types where absence is valid. Do not wrap method parameters, local variables, or fields in Optional.
import java.util.Optional;
public class AntiPattern6Demo {
public static void main(String[] args) {
// BAD: wrapping local variables
Optional name = Optional.of("Alice");
Optional age = Optional.of(30);
System.out.println(name.get() + " is " + age.get());
// This is absurd. You KNOW these values are not null.
// You just created them on the previous line!
// GOOD: use plain variables when you know the value exists
String name2 = "Alice";
int age2 = 30;
System.out.println(name2 + " is " + age2);
// Output:
// Alice is 30
// Alice is 30
}
}
Because orElse() is eagerly evaluated, putting a method call with side effects inside it is dangerous. The method runs even when the Optional has a value.
import java.util.Optional;
public class AntiPattern7Demo {
private static int counter = 0;
public static void main(String[] args) {
Optional name = Optional.of("Alice");
// BAD: side effect in orElse -- runs even though Optional is present
String result = name.orElse(createDefaultUser());
System.out.println("Result: " + result);
System.out.println("Counter: " + counter); // counter was incremented!
// Output:
// Creating default user...
// Result: Alice
// Counter: 1
counter = 0; // reset
// GOOD: use orElseGet for methods with side effects
String result2 = name.orElseGet(() -> createDefaultUser());
System.out.println("Result2: " + result2);
System.out.println("Counter2: " + counter); // counter was NOT incremented
// Output:
// Result2: Alice
// Counter2: 0
}
static String createDefaultUser() {
System.out.println(" Creating default user...");
counter++;
return "DefaultUser";
}
}
If your method signature promises Optional<T>, never return null. This is the worst possible pattern because callers trust that they can safely call methods on the return value without null checking.
import java.util.Optional;
public class AntiPattern8Demo {
// BAD: method says Optional but returns null
static Optional findUserBad(int id) {
if (id < 0) return null; // TERRIBLE -- breaks the contract
if (id == 1) return Optional.of("Alice");
return Optional.empty();
}
// GOOD: always return Optional.empty(), never null
static Optional findUserGood(int id) {
if (id < 0) return Optional.empty(); // correct!
if (id == 1) return Optional.of("Alice");
return Optional.empty();
}
public static void main(String[] args) {
// Caller trusts the Optional contract
try {
String name = findUserBad(-1).orElse("Unknown");
// NPE! Because findUserBad returned null, not Optional.empty()
} catch (NullPointerException e) {
System.out.println("NPE because method returned null instead of Optional.empty()");
}
// Output: NPE because method returned null instead of Optional.empty()
// With the good version, everything works
String name = findUserGood(-1).orElse("Unknown");
System.out.println("Good: " + name);
// Output: Good: Unknown
}
}
| # | Anti-Pattern | Why It Is Wrong | Correct Approach |
|---|---|---|---|
| 1 | isPresent() + get() |
Just a null check with more code | Use ifPresent(), map(), orElse() |
| 2 | Optional.of(null) |
Throws NPE immediately | Use Optional.ofNullable() |
| 3 | Optional as field | Not Serializable, memory overhead | Nullable field + Optional getter |
| 4 | Optional as parameter | Forces caller to wrap | Method overloading |
| 5 | Optional in collections | Every element needs unwrapping | Filter out nulls |
| 6 | Wrapping everything | Unnecessary overhead, hides intent | Use only for return types |
| 7 | orElse() with side effects |
Runs even when value is present | Use orElseGet() |
| 8 | Returning null from Optional method |
Breaks the Optional contract | Return Optional.empty() |
Java provides three specialized Optional classes for primitives: OptionalInt, OptionalLong, and OptionalDouble. These avoid the autoboxing overhead of wrapping primitives in Optional<Integer>, Optional<Long>, and Optional<Double>.
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.stream.IntStream;
public class PrimitiveOptionalDemo {
public static void main(String[] args) {
// OptionalInt
OptionalInt maxAge = findMaxAge(new int[]{25, 30, 22, 35, 28});
maxAge.ifPresent(age -> System.out.println("Max age: " + age));
// Output: Max age: 35
OptionalInt emptyMax = findMaxAge(new int[]{});
System.out.println("Empty max: " + emptyMax.orElse(-1));
// Output: Empty max: -1
// OptionalLong
OptionalLong fileSize = getFileSize("/some/file.txt");
System.out.println("File size: " + fileSize.orElse(0L) + " bytes");
// Output: File size: 0 bytes
// OptionalDouble
OptionalDouble average = IntStream.of(10, 20, 30).average();
System.out.println("Average: " + average.orElse(0.0));
// Output: Average: 20.0
OptionalDouble emptyAvg = IntStream.of().average();
System.out.println("Empty average: " + emptyAvg.orElse(0.0));
// Output: Empty average: 0.0
// Key differences from Optional:
// - getAsInt(), getAsLong(), getAsDouble() instead of get()
// - No map(), flatMap(), filter() -- these are limited types
// - No or() method
// - Primarily returned by IntStream, LongStream, DoubleStream operations
// When to use primitive Optionals:
// - When working with streams of primitives (IntStream, etc.)
// - When performance matters and you want to avoid autoboxing
// - When the Optional comes from stream.max(), stream.min(), stream.average()
}
static OptionalInt findMaxAge(int[] ages) {
return IntStream.of(ages).max();
}
static OptionalLong getFileSize(String path) {
// Simplified: would normally check if file exists
return OptionalLong.empty();
}
}
| Primitive Optional | Value Method | Default Method | Typical Source |
|---|---|---|---|
OptionalInt |
getAsInt() |
orElse(int) |
IntStream.max(), .min(), .findFirst() |
OptionalLong |
getAsLong() |
orElse(long) |
LongStream.max(), .min(), .findFirst() |
OptionalDouble |
getAsDouble() |
orElse(double) |
IntStream.average(), DoubleStream.max() |
Note: Primitive Optionals have a much more limited API than Optional<T>. They lack map(), flatMap(), filter(), or(), and stream(). If you need these operations, use Optional<Integer> etc. and accept the autoboxing cost.
These rules summarize how experienced Java developers use Optional. Each one comes from real-world pain.
Optional was designed for method return types where absence is a valid outcome. Do not use it for parameters, fields, or constructor arguments.
If your method returns Optional<T>, returning null violates the contract. Always return Optional.empty() instead.
Use map(), flatMap(), filter(), ifPresent(), orElse(), and orElseGet() instead of the imperative isPresent() check.
If the fallback value involves computation, network calls, or database queries, always use orElseGet() (lazy) instead of orElse() (eager).
If a method returns a list, set, or map, return an empty collection when there are no results. Optional<List<T>> adds no value -- the empty list already represents "no results."
When absence is an error (not a normal case), use orElseThrow() with a meaningful exception. This is clearer than get().
Optional's map()/flatMap()/filter() chain replaces the nested-null-check pyramid. Write declarative chains instead of imperative nesting.
Do not wrap values that you know are never null. Do not use Optional for performance-sensitive code paths (it creates an extra object). Primitive Optionals (OptionalInt, etc.) avoid boxing but have a limited API.
| # | Do | Do Not |
|---|---|---|
| 1 | Optional<User> findById(int id) |
void process(Optional<User> user) |
| 2 | return Optional.empty() |
return null from an Optional method |
| 3 | opt.map(User::getName).orElse("N/A") |
if (opt.isPresent()) return opt.get().getName() |
| 4 | opt.orElseGet(() -> loadFromDB()) |
opt.orElse(loadFromDB()) when DB call is expensive |
| 5 | List<User> findAll() returning empty list |
Optional<List<User>> findAll() |
| 6 | opt.orElseThrow(() -> new NotFoundException(id)) |
opt.get() hoping it is present |
| 7 | opt.flatMap(User::getAddress).map(Address::getCity) |
Nested if (user != null) { if (addr != null) { ... } } |
| 8 | Plain String name = "Alice" |
Optional<String> name = Optional.of("Alice") |
This example builds a user profile system that demonstrates every Optional concept covered in this tutorial. The domain model uses nullable fields with Optional getters (the correct pattern), and the service layer chains Optional operations to safely navigate the data.
import java.util.*;
import java.util.stream.Collectors;
// ===== DOMAIN MODEL =====
// Fields are nullable. Getters return Optional. This is the correct pattern.
class City {
private String name;
private String zipCode;
City(String name, String zipCode) {
this.name = name;
this.zipCode = zipCode;
}
String getName() { return name; }
String getZipCode() { return zipCode; }
}
class Address {
private String street;
private City city;
Address(String street, City city) {
this.street = street;
this.city = city;
}
String getStreet() { return street; }
Optional getCity() { return Optional.ofNullable(city); }
}
class UserProfile {
private String name;
private String email;
private String nickname;
private Address address;
private String phoneNumber;
UserProfile(String name, String email, String nickname,
Address address, String phoneNumber) {
this.name = name;
this.email = email;
this.nickname = nickname;
this.address = address;
this.phoneNumber = phoneNumber;
}
String getName() { return name; }
String getEmail() { return email; }
Optional getNickname() { return Optional.ofNullable(nickname); }
Optional getAddress() { return Optional.ofNullable(address); }
Optional getPhoneNumber() { return Optional.ofNullable(phoneNumber); }
}
// ===== REPOSITORY =====
class UserRepository {
private Map users = new HashMap<>();
UserRepository() {
users.put(1, new UserProfile(
"Alice Smith", "alice@example.com", "Ally",
new Address("123 Main St", new City("Springfield", "62701")),
"555-0101"
));
users.put(2, new UserProfile(
"Bob Johnson", "bob@example.com", null,
new Address("456 Oak Ave", null), // no city
null // no phone
));
users.put(3, new UserProfile(
"Charlie Brown", "charlie@example.com", "Chuck",
null, // no address at all
"555-0303"
));
users.put(4, new UserProfile(
"Diana Prince", "diana@example.com", null,
new Address("789 Hero Blvd", new City("Metropolis", "10001")),
null
));
}
// Returns Optional because user might not exist
Optional findById(int id) {
return Optional.ofNullable(users.get(id));
}
// Returns List (never Optional) -- empty list = no results
List findAll() {
return new ArrayList<>(users.values());
}
}
// ===== SERVICE =====
class UserService {
private UserRepository repository;
UserService(UserRepository repository) {
this.repository = repository;
}
// map() chain: User -> Address -> City -> name
String getCityName(int userId) {
return repository.findById(userId)
.flatMap(UserProfile::getAddress)
.flatMap(Address::getCity)
.map(City::getName)
.orElse("Unknown city");
}
// flatMap + map chain: User -> Address -> City -> zipCode
String getZipCode(int userId) {
return repository.findById(userId)
.flatMap(UserProfile::getAddress)
.flatMap(Address::getCity)
.map(City::getZipCode)
.orElse("N/A");
}
// map with transformation: extract domain from email
String getEmailDomain(int userId) {
return repository.findById(userId)
.map(UserProfile::getEmail)
.filter(email -> email.contains("@"))
.map(email -> email.substring(email.indexOf('@') + 1))
.orElse("unknown domain");
}
// Nickname with fallback to first name
String getDisplayName(int userId) {
return repository.findById(userId)
.flatMap(user -> user.getNickname()
.or(() -> Optional.of(user.getName().split(" ")[0])))
.orElse("Guest");
}
// Build mailing label using Optional chaining
String getMailingLabel(int userId) {
return repository.findById(userId)
.map(user -> {
StringBuilder label = new StringBuilder();
label.append(user.getName());
user.getAddress().ifPresent(addr -> {
label.append("\n").append(addr.getStreet());
addr.getCity().ifPresent(city ->
label.append("\n").append(city.getName())
.append(", ").append(city.getZipCode())
);
});
return label.toString();
})
.orElse("No mailing address available");
}
// orElseThrow: user MUST exist
UserProfile getRequiredUser(int userId) {
return repository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException(
"User not found: " + userId));
}
// Stream + Optional: find all users in a specific city
List findUsersInCity(String cityName) {
return repository.findAll().stream()
.filter(user -> user.getAddress()
.flatMap(Address::getCity)
.map(City::getName)
.filter(name -> name.equalsIgnoreCase(cityName))
.isPresent())
.map(UserProfile::getName)
.collect(Collectors.toList());
}
// Stream + Optional: collect all unique email domains
List getAllEmailDomains() {
return repository.findAll().stream()
.map(UserProfile::getEmail)
.filter(email -> email.contains("@"))
.map(email -> email.substring(email.indexOf('@') + 1))
.distinct()
.sorted()
.collect(Collectors.toList());
}
// Stream + Optional: contact info summary
String getContactSummary(int userId) {
return repository.findById(userId)
.map(user -> {
List contacts = new ArrayList<>();
contacts.add("Email: " + user.getEmail());
user.getPhoneNumber().ifPresent(p -> contacts.add("Phone: " + p));
user.getAddress().ifPresent(a ->
contacts.add("Address: " + a.getStreet()));
return user.getName() + " - " + String.join(", ", contacts);
})
.orElse("User not found");
}
}
// ===== MAIN =====
public class UserProfileSystem {
public static void main(String[] args) {
UserRepository repo = new UserRepository();
UserService service = new UserService(repo);
System.out.println("===== City Names =====");
System.out.println("User 1: " + service.getCityName(1));
System.out.println("User 2: " + service.getCityName(2));
System.out.println("User 3: " + service.getCityName(3));
System.out.println("User 99: " + service.getCityName(99));
System.out.println("\n===== Zip Codes =====");
System.out.println("User 1: " + service.getZipCode(1));
System.out.println("User 2: " + service.getZipCode(2));
System.out.println("\n===== Display Names (nickname or first name) =====");
System.out.println("User 1 (has nickname): " + service.getDisplayName(1));
System.out.println("User 2 (no nickname): " + service.getDisplayName(2));
System.out.println("User 3 (has nickname): " + service.getDisplayName(3));
System.out.println("\n===== Email Domains =====");
System.out.println("User 1: " + service.getEmailDomain(1));
System.out.println("All domains: " + service.getAllEmailDomains());
System.out.println("\n===== Mailing Labels =====");
System.out.println("--- User 1 (full address) ---");
System.out.println(service.getMailingLabel(1));
System.out.println("--- User 2 (no city) ---");
System.out.println(service.getMailingLabel(2));
System.out.println("--- User 3 (no address) ---");
System.out.println(service.getMailingLabel(3));
System.out.println("\n===== Users in Springfield =====");
System.out.println(service.findUsersInCity("Springfield"));
System.out.println("\n===== Contact Summaries =====");
for (int id = 1; id <= 4; id++) {
System.out.println(service.getContactSummary(id));
}
System.out.println("\n===== Required User (throws if not found) =====");
UserProfile alice = service.getRequiredUser(1);
System.out.println("Required: " + alice.getName());
try {
service.getRequiredUser(99);
} catch (IllegalArgumentException e) {
System.out.println("Expected error: " + e.getMessage());
}
}
}
// ===== City Names ===== // User 1: Springfield // User 2: Unknown city // User 3: Unknown city // User 99: Unknown city // // ===== Zip Codes ===== // User 1: 62701 // User 2: N/A // // ===== Display Names (nickname or first name) ===== // User 1 (has nickname): Ally // User 2 (no nickname): Bob // User 3 (has nickname): Chuck // // ===== Email Domains ===== // User 1: example.com // All domains: [example.com] // // ===== Mailing Labels ===== // --- User 1 (full address) --- // Alice Smith // 123 Main St // Springfield, 62701 // --- User 2 (no city) --- // Bob Johnson // 456 Oak Ave // --- User 3 (no address) --- // Charlie Brown // // ===== Users in Springfield ===== // [Alice Smith] // // ===== Contact Summaries ===== // Alice Smith - Email: alice@example.com, Phone: 555-0101, Address: 123 Main St // Bob Johnson - Email: bob@example.com, Address: 456 Oak Ave // Charlie Brown - Email: charlie@example.com, Phone: 555-0303 // Diana Prince - Email: diana@example.com, Address: 789 Hero Blvd // // ===== Required User (throws if not found) ===== // Required: Alice Smith // Expected error: User not found: 99
| # | Concept | Where Used |
|---|---|---|
| 1 | Optional.ofNullable() |
Repository findById(), all nullable getters |
| 2 | Optional.empty() |
Implicit when ofNullable receives null |
| 3 | map() |
getCityName(), getEmailDomain(), getContactSummary() |
| 4 | flatMap() |
getCityName(), getZipCode(), findUsersInCity() |
| 5 | filter() |
getEmailDomain(), findUsersInCity() |
| 6 | orElse() |
All service methods with string defaults |
| 7 | orElseThrow() |
getRequiredUser() |
| 8 | ifPresent() |
getMailingLabel(), getContactSummary() |
| 9 | or() (Java 9+) |
getDisplayName() -- nickname or first name fallback |
| 10 | Nullable field + Optional getter | All domain classes (UserProfile, Address) |
| 11 | Stream + Optional integration | findUsersInCity(), getAllEmailDomains() |
| 12 | Return List not Optional of List | findAll(), findUsersInCity(), getAllEmailDomains() |
Every Optional method, organized by category, with the Java version it was introduced.
| Category | Method | Description | Java |
|---|---|---|---|
| Creation | Optional.of(value) |
Wraps non-null value; throws NPE if null | 8 |
Optional.ofNullable(value) |
Wraps value; returns empty if null | 8 | |
Optional.empty() |
Returns an empty Optional | 8 | |
| Checking | isPresent() |
Returns true if value is present | 8 |
isEmpty() |
Returns true if value is absent | 11 | |
| Getting | get() |
Returns value or throws NoSuchElementException (avoid) | 8 |
orElse(default) |
Returns value or default (eager evaluation) | 8 | |
orElseGet(supplier) |
Returns value or supplier result (lazy evaluation) | 8 | |
orElseThrow(supplier) |
Returns value or throws supplied exception | 8 | |
orElseThrow() |
Returns value or throws NoSuchElementException | 10 | |
or(supplier) |
Returns this Optional or supplier's Optional if empty | 9 | |
| Transforming | map(function) |
Transforms value if present; wraps result in Optional | 8 |
flatMap(function) |
Transforms value; function must return Optional | 8 | |
filter(predicate) |
Returns this if predicate matches; empty otherwise | 8 | |
| Consuming | ifPresent(consumer) |
Runs consumer if value is present | 8 |
ifPresentOrElse(consumer, emptyAction) |
Runs consumer if present; runs emptyAction if empty | 9 | |
| Stream | stream() |
Returns a one-element or empty Stream | 9 |
| Identity | equals(obj) |
Compares contained values | 8 |
hashCode() |
Hash of contained value or 0 | 8 | |
toString() |
Returns "Optional[value]" or "Optional.empty" | 8 |