JAVA / STREAMS, LAMBDAS AND OPTIONAL
Optional for modelling absence without null
Model 'no result' with Optional so callers cannot ignore it: build with ofNullable, chain map, filter and flatMap, and unwrap once with orElse or orElseThrow.
What you will learn
- Return Optional.ofNullable(x) from lookups that can find nothing, never null
- Chain map, filter and flatMap so the empty case short-circuits with no null checks
- Choose orElse, orElseGet or orElseThrow by default cost and whether empty is a bug
- Keep Optional in return types; avoid it for fields, parameters and collection elements
Understanding Optional for modelling absence without null
A method that returns null lies by omission: the signature promises a String, the runtime may hand back nothing, and nothing in the middle forces the caller to check. Declaring Optional<String> moves that fact into the type, so the caller must open the container before touching a value and the compiler is what reminds them. Optional.ofNullable is the bridge from the older world of possibly-null results, typically a map lookup or a query, while Optional.of is an assertion that the value is really there and throws NullPointerException on the spot if it is not.
The useful mental model is a box holding zero or one element. map applies a function inside the box and re-boxes the result with ofNullable semantics, so a function that returns null collapses the box to empty; flatMap is for functions that already hand you a box, and it keeps one layer instead of Optional<Optional<T>>. filter can empty a full box when the value is present but unusable, such as a blank string. Every one of these is a no-op on an empty box, which is why such a chain contains no branches at all: absence propagates by itself.
You open the box once, at the point that knows what absence means. orElse fits a cheap constant, orElseGet fits a default that costs something because its supplier only runs when the box is empty, orElseThrow fits the case where empty means the caller is broken, and ifPresent or ifPresentOrElse fit a side effect rather than a value. Note that orElse's argument is an ordinary Java argument, evaluated before the call, which is exactly why orElseGet exists. Optional is designed for return values where finding nothing is a normal outcome; as a field or parameter type it only adds a wrapper to unpack, and for a list of results an empty list already says what Optional would.
placeholder
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class Main {
record User(String name, String email) {}
static final Map<Integer, User> USERS = new HashMap<>();
static {
USERS.put(1, new User("Ada", "ada@example.com"));
USERS.put(2, new User("Grace", null));
}
static Optional<User> findUser(int id) {
return Optional.ofNullable(USERS.get(id));
}
static String emailDomain(int id) {
return findUser(id)
.map(User::email)
.filter(e -> e.contains("@"))
.map(e -> e.substring(e.indexOf('@') + 1))
.orElse("unknown");
}
public static void main(String[] args) {
System.out.println(emailDomain(1));
System.out.println(emailDomain(2));
System.out.println(emailDomain(3));
System.out.println(findUser(1));
System.out.println(findUser(9));
findUser(1).ifPresentOrElse(
u -> System.out.println("greeting " + u.name()),
() -> System.out.println("nobody to greet"));
}
}Optional puts the possibility of absence into a method's return type, so the empty case is handled by the caller's code instead of discovered as a NullPointerException.
Worked examples
orElse evaluates eagerly, orElseGet does not
Shows that the default handed to orElse is computed even when the value is present, while orElseGet defers it behind a supplier.
import java.util.Optional;
public class Main {
static String expensive() {
System.out.println("computing default");
return "default";
}
public static void main(String[] args) {
Optional<String> present = Optional.of("value");
System.out.println(present.orElse(expensive()));
System.out.println(present.orElseGet(Main::expensive));
Optional<String> empty = Optional.empty();
System.out.println(empty.orElseGet(Main::expensive));
try {
empty.orElseThrow(() -> new IllegalStateException("no value"));
} catch (IllegalStateException e) {
System.out.println("caught " + e.getMessage());
}
}
}Example explained
Line 1expensive() sits in the argument list of orElse, so it runs before orElse is entered and prints even though the value is present.
Line 2orElseGet receives a Supplier, so Main::expensive is never invoked on the present Optional and only "value" prints.
Line 3On the empty Optional the same supplier does run, which is why "computing default" appears immediately before "default".
Line 4orElseThrow also builds its exception lazily, and it is the right ending when an empty Optional means the caller is wrong.
flatMap for functions that already return Optional
Demonstrates why map nests containers while flatMap keeps a single layer, and how or supplies a second source of values.
import java.util.Map;
import java.util.Optional;
public class Main {
static final Map<String, String> ADDRESSES = Map.of("Ada", "EC1A 1BB");
static final Map<String, String> CACHE = Map.of();
static final Map<String, String> DATABASE = Map.of("k", "stored value");
static Optional<String> address(String name) {
return Optional.ofNullable(ADDRESSES.get(name));
}
static Optional<String> postcode(String address) {
int space = address.indexOf(' ');
return space < 0 ? Optional.empty() : Optional.of(address.substring(0, space));
}
public static void main(String[] args) {
System.out.println(address("Ada").flatMap(Main::postcode).orElse("no postcode"));
System.out.println(address("Grace").flatMap(Main::postcode).orElse("no postcode"));
Optional<Optional<String>> nested = address("Ada").map(Main::postcode);
System.out.println(nested);
System.out.println(Optional.ofNullable(CACHE.get("k"))
.or(() -> Optional.ofNullable(DATABASE.get("k")))
.orElseThrow());
}
}Example explained
Line 1postcode already returns Optional<String>, so flatMap splices that result in and the chain stays Optional<String>.
Line 2address("Grace") is empty, so flatMap never calls postcode and orElse supplies the fallback text.
Line 3Passing the same method to map yields Optional[Optional[EC1A]], which is proof that map always adds one layer.
Line 4or takes a Supplier<Optional<String>>, so the database map is only consulted after the cache misses.
empty is not null
Clarifies the difference between ofNullable and of, how empty Optionals compare, and what get does on an empty one.
import java.util.NoSuchElementException;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
String missing = null;
System.out.println(Optional.ofNullable(missing).isPresent());
System.out.println(Optional.empty().equals(Optional.ofNullable(missing)));
System.out.println(Optional.of("hi").equals("hi"));
try {
Optional.of(missing);
} catch (NullPointerException e) {
System.out.println("of(null) rejected");
}
try {
Optional.ofNullable(missing).get();
} catch (NoSuchElementException e) {
System.out.println("get failed: " + e.getMessage());
}
}
}Example explained
Line 1ofNullable(null) produces the empty Optional instead of throwing, which is how you adapt any null-returning API.
Line 2Two empty Optionals are equal because equality compares contents, not the declared type argument.
Line 3An Optional is never equal to the value inside it, so comparing Optional.of("hi") with "hi" is false.
Line 4of(null) fails fast with NullPointerException, while get on an empty Optional fails later with NoSuchElementException.
Important notes
A method whose return type is Optional must never return null; callers chain directly onto the result, so a null there breaks at the very call that was supposed to make absence safe.
or, ifPresentOrElse and Optional.stream() need Java 9, and the no-argument orElseThrow() needs Java 10; on Java 8 use isPresent together with orElseGet.
Common mistakes
Calling get() (or the no-argument orElseThrow()) without knowing the value is there: you have only traded NullPointerException for NoSuchElementException at the same line, with less information than before.
Using Optional.of() on a value that might be null: it throws NullPointerException while constructing the wrapper, so the code fails before it can model absence at all; ofNullable is the factory that accepts null.
Writing orElse(loadDefault()) instead of orElseGet(this::loadDefault): the default is computed on every call including the present case, so logging, queries or exceptions in the default path leak into the happy path.
Try it yourself
Change, predict, then run
Build a Map<String, Integer> with two scores and write Optional<Integer> findScore(String name) using ofNullable, then print a result for "ada" and for "nobody" using map, filter(s -> s >= 50) and orElse("no grade") with no if statement and no call to get().
Open the Java workspaceCheck your understanding
A helper Optional<String> nickname(User u) already returns an Optional. Given Optional<User> maybe, what does maybe.map(Main::nickname) produce, and why?
- Optional<String>, because map unwraps any Optional that the function returns
- Optional<String>, but it throws NoSuchElementException when maybe is empty
- Optional<Optional<String>>, because map re-boxes whatever the function returns; flatMap is the one that keeps a single layer
- Optional<String>, and nickname is called with null when maybe is empty
Show answer
map means "apply the function inside the box and box the result again", and here the result is already a box, so you end up with two layers; flatMap exists precisely because it adopts the returned Optional instead of wrapping it. Option 1 is tempting because an empty Optional feels like it should fail, but map on an empty Optional simply returns empty and never calls the function.