JAVA / STREAMS, LAMBDAS AND OPTIONAL
Chaining Optionals and avoiding isPresent abuse
Chain Optional results with map, flatMap, filter and or, and end the chain with orElseGet or orElseThrow instead of gating get() behind isPresent().
What you will learn
- Use flatMap when a method already returns Optional; use map when it returns a value
- Replace every isPresent/get pair with one chain ending in orElseGet or orElseThrow
- Remember orElse evaluates its argument even when the Optional holds a value
- Rely on short-circuiting: on an empty Optional, later map and filter lambdas never run
Understanding Chaining Optionals and avoiding isPresent abuse
Optional offers three shaping operations over at most one element: map applies a function and rewraps the result with ofNullable, flatMap expects the function to already return an Optional and hands that Optional back unchanged, and filter turns a present value that fails a predicate into an empty one. Every stage is a no-op on an empty Optional, so the lambda never sees null and never runs at all. That is the whole mechanism behind chaining: you describe the path through the data as if all the values were there, and absence is carried forward for you.
isPresent() followed by get() rebuilds the null check you were trying to remove, in a form the compiler cannot verify. Nothing ties the guard to the unwrap, so the guard can be widened to a.isPresent() || b.isPresent(), moved into an outer block, or copied to a new branch while a get() is left behind to throw NoSuchElementException at runtime. Nested lookups make it worse: three nullable hops become three levels of if, while the equivalent chain is one expression with exactly one place where absence is handled.
That ending is a real decision, not boilerplate. orElse takes an already-computed value, so its argument is evaluated on every call even when a value is present; orElseGet takes a Supplier and only runs it when empty; orElseThrow declares that absence is a bug or a broken contract; or() keeps you inside Optional so you can try a second source; ifPresentOrElse covers the case where both branches are side effects. Choose by what absence means at that point in the code, and let everything above it stay a pure chain.
import java.util.Optional;
public class OptionalChaining {
static final class Address {
private final String zip;
Address(String zip) { this.zip = zip; }
Optional<String> zip() { return Optional.ofNullable(zip); }
}
static final class Customer {
private final Address address;
Customer(Address address) { this.address = address; }
Optional<Address> address() { return Optional.ofNullable(address); }
}
// One expression: no null checks, no isPresent, no get.
static Optional<String> shippingLabel(Customer c) {
return Optional.ofNullable(c)
.flatMap(Customer::address) // already returns Optional -> flatMap
.flatMap(Address::zip) // already returns Optional -> flatMap
.filter(z -> z.length() == 5) // present but invalid becomes empty
.map(z -> "ZIP-" + z); // returns a plain value -> map
}
public static void main(String[] args) {
Customer ada = new Customer(new Address("90210"));
Customer bob = new Customer(new Address("902"));
Customer cy = new Customer(new Address(null));
Customer dee = new Customer(null);
System.out.println("ada -> " + shippingLabel(ada).orElse("no label"));
System.out.println("bob -> " + shippingLabel(bob).orElse("no label"));
System.out.println("cy -> " + shippingLabel(cy).orElse("no label"));
System.out.println("dee -> " + shippingLabel(dee).orElse("no label"));
System.out.println("null -> " + shippingLabel(null).orElse("no label"));
Optional<String> none = Optional.<String>empty()
.map(z -> { System.out.println("never printed"); return z.trim(); });
System.out.println("chain stayed empty: " + none.isEmpty());
}
}An Optional is something you compose with map, flatMap and filter until one final stage decides what absence means, not a box you test and then unwrap.
Worked examples
orElse versus orElseGet
Shows that orElse computes its fallback even when the Optional already holds a value, while orElseGet does not.
import java.util.Optional;
public class LazyFallback {
static String fallback() {
System.out.println("computing fallback");
return "generated";
}
public static void main(String[] args) {
Optional<String> stored = Optional.of("stored");
System.out.println("orElse: " + stored.orElse(fallback()));
System.out.println("orElseGet: " + stored.orElseGet(LazyFallback::fallback));
Optional<String> missing = Optional.empty();
System.out.println("absent: " + missing.orElseGet(LazyFallback::fallback));
}
}Example explained
Line 1fallback() is an ordinary argument expression, so Java runs it before orElse is even entered, which is why the first printed line comes from the fallback and not from the result.
Line 2orElseGet receives a Supplier, so on a present Optional the method body is never invoked and the second result line prints nothing extra.
Line 3Only the third call is empty, so there the supplier finally runs and its return value becomes the result.
Falling through to a second source with or()
Uses or() to try a second lookup without leaving Optional, and ifPresentOrElse to handle both branches as effects.
import java.util.Map;
import java.util.Optional;
public class Fallbacks {
static final Map<String, String> CACHE = Map.of("theme", "dark");
static final Map<String, String> CONFIG = Map.of("locale", "en_GB");
static Optional<String> setting(String key) {
return Optional.ofNullable(CACHE.get(key))
.or(() -> Optional.ofNullable(CONFIG.get(key)));
}
public static void main(String[] args) {
for (String key : new String[] {"theme", "locale", "font"}) {
setting(key).ifPresentOrElse(
value -> System.out.println(key + " = " + value),
() -> System.out.println(key + " is unset"));
}
Optional<String> mapped = Optional.of("theme").map(CONFIG::get);
System.out.println("map with a null result is empty: " + mapped.isEmpty());
}
}Example explained
Line 1or() takes a Supplier<Optional<String>> and returns an Optional, so a cache miss falls through to CONFIG while the value stays wrapped for the caller.
Line 2The supplier passed to or() is skipped entirely for the "theme" key, because the first lookup was already present.
Line 3ifPresentOrElse gives both outcomes a place to live, which is what replaces an if (opt.isPresent()) ... else ... block when both sides are side effects.
Line 4CONFIG.get("theme") returns null, and map rewraps mapper results with ofNullable, so mapped is empty rather than an Optional containing null.
How an isPresent guard drifts away from its get
Contrasts a guard that does not actually prove presence with a composition that requires both values.
import java.util.NoSuchElementException;
import java.util.Optional;
public class GetTrap {
static Optional<Integer> parse(String s) {
try {
return Optional.of(Integer.valueOf(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
static String sum(Optional<Integer> x, Optional<Integer> y) {
return x.flatMap(i -> y.map(j -> i + j))
.map(t -> "sum = " + t)
.orElse("sum unavailable");
}
public static void main(String[] args) {
Optional<Integer> a = parse("40");
Optional<Integer> b = parse("two");
if (a.isPresent() || b.isPresent()) { // proves nothing about b
try {
System.out.println("sum = " + (a.get() + b.get()));
} catch (NoSuchElementException e) {
System.out.println("crashed: " + e.getMessage());
}
}
System.out.println(sum(a, parse("2")));
System.out.println(sum(a, b));
}
}Example explained
Line 1The || guard is true because a is present, and the compiler does not link that guard to either unwrap, so b.get() throws NoSuchElementException with the message "No value present".
Line 2In sum, flatMap on x with map on y inside requires both values, so if either side is empty the whole expression is empty and no exception is possible.
Line 3Both failure modes reach the same orElse, which is where the single "sum unavailable" answer is written.
Important notes
A chain reports only that the result is empty, never which hop produced the emptiness; when the caller needs the reason, keep that check explicit or throw a specific exception from orElseThrow at the stage that fails.
isPresent() and isEmpty() are not banned, they are the right answer when the question really is a boolean; the abuse is using them as a gate in front of get(). Note that or() and ifPresentOrElse() need Java 9, no-argument orElseThrow() needs Java 10, and isEmpty() needs Java 11.
Common mistakes
Calling map with a method that already returns Optional, which produces Optional<Optional<String>>; the next stage then either fails to compile or forces a second unwrap that flatMap would have avoided.
Writing Optional.of(map.get(key)) for a value that can be null: of() throws NullPointerException before any of your absence handling runs, and ofNullable is the factory meant for maybe-null values.
Passing a call into orElse, such as orElse(loadDefaults()): loadDefaults() executes on every invocation, present or not, so its cost and any side effects happen even when the Optional had a value.
Try it yourself
Change, predict, then run
Write Optional<String> initials(Optional<String> first, Optional<String> last) that returns a value like "A.T." only when both names are present and not blank, using only flatMap, map and filter. Run it against Optional.of("Alan") with Optional.of("Turing"), then with Optional.of(" "), then with Optional.empty(), without writing isPresent or get anywhere.
Open the Java workspaceCheck your understanding
A repository returns Optional<User>, and User.email() returns Optional<String>. Why does u.map(User::email).filter(s -> s.contains("@")) fail to compile while u.flatMap(User::email).filter(s -> s.contains("@")) works?
- map rewraps whatever the mapper returns, so the type becomes Optional<Optional<String>> and filter's predicate receives an Optional, which has no contains method
- map only accepts lambdas, so a method reference such as User::email must be passed to flatMap instead
- map throws NoSuchElementException when the user is absent, and the compiler rejects chains that can throw
- filter must appear before any mapping stage in an Optional chain
Show answer
map applies the mapper and wraps the result with ofNullable, so a mapper that already produces Optional<String> yields a doubly wrapped Optional<Optional<String>>, and the predicate is then handed an Optional; flatMap returns the mapper's Optional unchanged, giving Optional<String>. The NoSuchElementException option confuses runtime absence with typing: an empty Optional simply skips the mapper, nothing is thrown, and possible exceptions never affect compilation.