Skip to main content

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. Use Optional.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 OptionalUse null
Method return types where "no value" is a valid outcomePrivate 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 transformationsCollections (use empty collection instead of null)

Rule of thumb

  • Return Optional from methods that might not have a result
  • Accept null (not Optional) in method parameters -- use @Nullable annotation
  • Never store Optional as a field (it is not Serializable and 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

PitfallProblemFix
Optional.of(null)Throws NullPointerException immediatelyUse Optional.ofNullable(value)
Optional.get() without checkThrows NoSuchElementExceptionUse orElse(), orElseThrow(), or ifPresent()
Optional as fieldNot Serializable; memory overheadStore null, expose Optional via getter
orElse() with expensive defaultorElse(expensiveCall()) always evaluates the argumentUse orElseGet(() -> expensiveCall()) for lazy evaluation
Optional in collectionsList<Optional<String>> is confusingFilter nulls before collecting, or use flatMap(Optional::stream)
Nested OptionalsOptional<Optional<T>> from map returning OptionalUse flatMap instead of map

See also