Optionals: Avoiding NullPointerException
Optional<T> is a container that may or may not hold a value. It forces callers to
explicitly handle the "no value" case instead of silently passing null around until
something throws a NullPointerException.
Quick start
// Returns Optional instead of nullable String
Optional<String> findUserName(int id) {
User user = database.findById(id);
return Optional.ofNullable(user).map(User::name);
}
// Caller is forced to handle the empty case
String name = findUserName(42).orElse("Unknown");
Creating Optionals
// Non-null value (throws NullPointerException if null!)
Optional<String> present = Optional.of("hello");
// Nullable value (empty if null)
Optional<String> maybe = Optional.ofNullable(getNullableValue());
// Empty optional
Optional<String> empty = Optional.empty();
Use
Optional.of()when you are certain the value is non-null. UseOptional.ofNullable()when the value might be null.
Unwrapping values
Safe unwrapping
Optional<String> opt = Optional.of("hello");
// Default value
String val1 = opt.orElse("default");
// Lazy default (only computed if empty)
String val2 = opt.orElseGet(() -> expensiveComputation());
// Throw if empty
String val3 = opt.orElseThrow(); // NoSuchElementException
String val4 = opt.orElseThrow(() -> new NotFoundException("User not found"));
// Conditional execution
opt.ifPresent(System.out::println);
// Handle both cases (Java 9+)
opt.ifPresentOrElse(
value -> System.out.println("Found: " + value),
() -> System.out.println("Not found")
);
Avoid: get() without checking
// BAD -- throws NoSuchElementException if empty
String value = opt.get();
// If you must use get(), at least check first
if (opt.isPresent()) {
String value = opt.get();
}
// But this defeats the purpose. Use orElse/orElseThrow instead.
Chaining operations
map
Transform the value if present:
Optional<String> name = Optional.of(" Alice ");
Optional<String> cleaned = name
.map(String::strip)
.map(String::toUpperCase);
// Optional[ALICE]
// If empty, map is skipped
Optional<String> empty = Optional.<String>empty()
.map(String::toUpperCase);
// Optional.empty
flatMap
Use when the transformation itself returns an Optional (avoids Optional<Optional<T>>):
Optional<User> findUser(int id) { ... }
Optional<Address> getAddress(User user) { ... }
// map would give Optional<Optional<Address>> -- wrong!
// flatMap unwraps the inner Optional
Optional<String> city = findUser(42)
.flatMap(this::getAddress)
.map(Address::city);
filter
Keep the value only if it matches a predicate:
Optional<Integer> age = Optional.of(25);
Optional<Integer> adult = age.filter(a -> a >= 18);
// Optional[25]
Optional<Integer> senior = age.filter(a -> a >= 65);
// Optional.empty
or (Java 9+)
Provide an alternative Optional if empty:
Optional<String> primary = findInCache(key);
Optional<String> fallback = primary.or(() -> findInDatabase(key));
stream (Java 9+)
Convert to a Stream (0 or 1 elements), useful in stream pipelines:
List<Optional<String>> optionals = List.of(
Optional.of("a"),
Optional.empty(),
Optional.of("b"),
Optional.empty(),
Optional.of("c")
);
List<String> values = optionals.stream()
.flatMap(Optional::stream)
.toList();
// [a, b, c]
Chaining example: user profile lookup
record User(String name, Optional<Address> address) {}
record Address(String street, String city, Optional<String> zipCode) {}
String getZipCode(Optional<User> user) {
return user
.flatMap(User::address)
.flatMap(Address::zipCode)
.map(String::strip)
.filter(zip -> !zip.isEmpty())
.orElse("N/A");
}
Optional with streams
// findFirst and findAny return Optional
Optional<String> firstLong = List.of("hi", "hello", "hey")
.stream()
.filter(s -> s.length() > 3)
.findFirst();
// Optional[hello]
// min and max return Optional
Optional<Integer> max = List.of(3, 7, 2, 9)
.stream()
.max(Integer::compareTo);
// Optional[9]
// reduce without identity returns Optional
Optional<Integer> sum = List.of(1, 2, 3)
.stream()
.reduce(Integer::sum);
// Optional[6]
Optional vs null: when to use which
| Use Optional | Use null |
|---|---|
| Method return types where "no value" is a valid outcome | Private fields (Optional has memory overhead) |
| API boundaries (public methods) | Performance-critical inner loops |
| Stream terminal operations (findFirst, min, max) | When the method contract clearly states non-null |
| When you want to chain transformations | Collections (use empty collection instead of null) |
Rule of thumb
- Return
Optionalfrom methods that might not have a result - Accept
null(not Optional) in method parameters -- use@Nullableannotation - Never store Optional as a field (it is not
Serializableand adds overhead) - Never use Optional in collections (
List<Optional<String>>is a code smell)
Anti-patterns
Using Optional as a field
// BAD
class User {
private Optional<String> middleName; // overhead, not serializable
}
// GOOD
class User {
private String middleName; // nullable
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
}
Optional as method parameter
// BAD -- forces callers to wrap values
void process(Optional<String> name) { ... }
process(Optional.of("Alice")); // clunky
// GOOD -- accept nullable, overload, or use a default
void process(String name) { ... }
void process() { process(null); }
Wrapping and immediately unwrapping
// BAD -- pointless
String value = Optional.ofNullable(getValue()).orElse("default");
// GOOD -- use a simple null check
String value = getValue();
if (value == null) value = "default";
// UNLESS you need to chain operations
String value = Optional.ofNullable(getValue())
.map(String::strip)
.filter(s -> !s.isEmpty())
.orElse("default");
// This is fine -- the chain justifies the Optional
Using isPresent() + get() instead of functional methods
// BAD -- imperative style defeats the purpose
if (optional.isPresent()) {
return optional.get().toUpperCase();
} else {
return "DEFAULT";
}
// GOOD
return optional.map(String::toUpperCase).orElse("DEFAULT");
Common pitfalls
| Pitfall | Problem | Fix |
|---|---|---|
Optional.of(null) | Throws NullPointerException immediately | Use Optional.ofNullable(value) |
Optional.get() without check | Throws NoSuchElementException | Use orElse(), orElseThrow(), or ifPresent() |
| Optional as field | Not Serializable; memory overhead | Store null, expose Optional via getter |
orElse() with expensive default | orElse(expensiveCall()) always evaluates the argument | Use orElseGet(() -> expensiveCall()) for lazy evaluation |
| Optional in collections | List<Optional<String>> is confusing | Filter nulls before collecting, or use flatMap(Optional::stream) |
| Nested Optionals | Optional<Optional<T>> from map returning Optional | Use flatMap instead of map |
See also
- Streams and Collectors -- stream operations that return Optional
- Error Handling -- Result/Either pattern as an alternative
- Modern Java Features -- records and pattern matching
- Functional Interfaces -- Supplier, Function used with Optional