Java Module 9 – Lambdas, functional interfaces and Optional
Java Optional done right
Chain Java Optional with map, flatMap and filter, choose orElse, orElseGet or orElseThrow from recorded call counts, and avoid the get() and field traps.
What you will learn
- Transform Optional values with map, flatMap and filter
- Choose between orElse, orElseGet, orElseThrow and ifPresentOrElse
- Avoid common Optional anti-patterns
Before you start
What you’ll learn · Before you start
What you will learn
- Transform Optional values with map, flatMap and filter
- Choose between orElse, orElseGet, orElseThrow and ifPresentOrElse
- Avoid common Optional anti-patterns
Before you start
On this page
Optional<T> holds either one value or nothing, and it exists to say so in a method’s return type: find(id)
returning Optional<User> tells every caller that there may be no user, which a User that may be null never
does. Using it well comes down to three habits. Transform the value inside it with map, flatMap and
filter, which run only when a value is present, so a chain of lookups needs no if at all. Take the value out
once, at the end, with the method whose cost and failure behaviour you want: orElse for a ready default,
orElseGet for a default that is expensive to make, orElseThrow when absence is an error. Keep it out of places
it was not designed for: fields, parameters and collections. The Optional Javadoc itself says that the type is
meant primarily for return values. This lesson runs each habit on JDK 25, including two mistakes that only show up
at run time: a default computed when nobody needed it, and an object that cannot be serialized because it holds an
Optional.
A lookup chain with flatMap
The directory below can fail to find a user, a user may have no address, and an address may have no postal code on
file. With null that is three checks. With Optional it is one chain:
/** postalCode may be null: not every address has one on file. */
record Address(String city, String postalCode) {} import java.util.Optional;
/** address may be null; addressIfKnown() says so with an Optional. */
record User(String id, String name, Address address) {
Optional<Address> addressIfKnown() {
return Optional.ofNullable(address);
}
} import java.util.Map;
import java.util.Optional;
class Directory {
private final Map<String, User> users = Map.of(
"u1", new User("u1", "Asha", new Address("Pune", "411001")),
"u2", new User("u2", "Ravi", null),
"u3", new User("u3", "Meera", new Address("Kochi", null)));
/** The user with this id, or an empty Optional. */
Optional<User> find(String id) {
return Optional.ofNullable(users.get(id));
}
} import java.util.List;
import java.util.Optional;
class Lookup {
static String postalCode(Directory directory, String id) {
return directory.find(id) // Optional<User>
.flatMap(User::addressIfKnown) // Optional<Address>: flatMap, because the step returns an Optional
.map(Address::postalCode) // Optional<String>: a null result becomes empty
.orElse("not known");
}
static void main() {
Directory directory = new Directory();
for (String id : List.of("u1", "u2", "u3", "u9")) {
IO.println(id + ": " + postalCode(directory, id));
}
// With map instead of flatMap, the step's own Optional ends up inside another one.
Optional<Optional<Address>> nested = directory.find("u1").map(User::addressIfKnown);
IO.println("map: " + nested);
IO.println("flatMap: " + directory.find("u1").flatMap(User::addressIfKnown));
IO.println("filter: " + directory.find("u1").filter(u -> u.name().startsWith("R")));
}
} The postal-code lookup as an Optional chain
Text description of the diagram
The diagram follows the lookup chain from top to bottom.
- directory.find(id) returns an Optional<User>. If a user is found, the chain continues; if not, it becomes an empty Optional.
- flatMap(User::addressIfKnown) turns the user into an Optional<Address>. flatMap is used because addressIfKnown already returns an Optional. If the user has no address, the result is an empty Optional.
- map(Address::postalCode) turns the address into an Optional<String>. If postalCode() returns null, map gives an empty Optional.
- orElse("not known") ends the chain with a String: the postal code when every step found a value, or the default "not known" when any step produced an empty Optional. After the first empty step, none of the later steps run.
from session import run
run("""
java Lookup.java
""") Output
# javac 25.0.4.1 $ java Lookup.java u1: 411001 u2: not known u3: not known u9: not known map: Optional[Optional[Address[city=Pune, postalCode=411001]]] flatMap: Optional[Address[city=Pune, postalCode=411001]] filter: Optional.empty
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lookup_session.py
Each id fails at a different step: u2 has no address, u3 has no postal code, u9 does not exist. The chain is
the same for all of them, because once a step produces an empty Optional, the later steps are skipped and
orElse supplies the default. The step that needed thought is the second. addressIfKnown() already returns an
Optional, so map would wrap it a second time, and the session printed that nested
Optional[Optional[Address[…]]]. flatMap is for steps that return an Optional themselves: it passes their
result through without wrapping it again. The third step uses map on a method that may return null, which is
safe, because map treats a null result as empty.
The methods that work on the value inside an Optional, and what each does with an empty one:
map(f)givesOptional.ofNullable(f(value))when a value is present, and stays empty without callingfotherwise. Use it for a step that returns a plain value, which may benull.flatMap(f)givesf(value), itself anOptional, when a value is present, and stays empty otherwise. Use it for a step that already returns anOptional.filter(p)keeps the value only ifp(value)is true, and never callspon an emptyOptional. Use it to keep a value only when it passes a test.or(supplier)(Java 9) keeps a present value, and askssupplierfor anotherOptionalwhen empty. Use it for a second place to look.stream()(Java 9) gives a stream of one element, or an empty stream. Use it to joinOptionals into a stream.
Getting the value out
The ways out differ in two things: what happens when the Optional is empty, and whether the default is computed
when it is not needed. The program counts calls of a deliberately slow default:
import java.util.NoSuchElementException;
import java.util.Optional;
class OrElse {
static int loads = 0;
/** Stands in for a slow call, such as a database query. */
static String loadDefault() {
loads++;
return "guest";
}
static void main() {
Optional<String> present = Optional.of("asha");
Optional<String> empty = Optional.empty();
loads = 0;
present.orElse(loadDefault());
IO.println("orElse, value present: " + loads + " call of loadDefault()");
loads = 0;
present.orElseGet(() -> loadDefault());
IO.println("orElseGet, value present: " + loads + " calls");
loads = 0;
String name = empty.orElseGet(() -> loadDefault());
IO.println("orElseGet, empty: " + loads + " call, result " + name);
try {
empty.orElseThrow();
} catch (NoSuchElementException e) {
IO.println("orElseThrow(): NoSuchElementException: " + e.getMessage());
}
try {
empty.orElseThrow(() -> new IllegalStateException("no user with id u9"));
} catch (IllegalStateException e) {
IO.println("orElseThrow(supplier): IllegalStateException: " + e.getMessage());
}
present.ifPresentOrElse(n -> IO.println("ifPresentOrElse, present: hello " + n), () -> IO.println("nobody"));
empty.ifPresentOrElse(n -> IO.println("hello " + n), () -> IO.println("ifPresentOrElse, empty: nobody"));
IO.println("or, empty: " + empty.or(() -> Optional.of("from the backup list")));
}
} import java.util.Optional;
class GetEmpty {
static void main() {
Optional<String> nickname = Optional.empty();
IO.println(nickname.get().length());
}
} from session import run
run("""
java OrElse.java
java GetEmpty.java
""", fails=("java GetEmpty.java",)) Output
# javac 25.0.4.1 $ java OrElse.java orElse, value present: 1 call of loadDefault() orElseGet, value present: 0 calls orElseGet, empty: 1 call, result guest orElseThrow(): NoSuchElementException: No value present orElseThrow(supplier): IllegalStateException: no user with id u9 ifPresentOrElse, present: hello asha ifPresentOrElse, empty: nobody or, empty: Optional[from the backup list] $ java GetEmpty.java Exception in thread "main" java.util.NoSuchElementException: No value present at java.base/java.util.Optional.get(Optional.java:143) at GetEmpty.main(GetEmpty.java:6) [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 orelse_session.py
orElse(value)calledloadDefault()although a value was present. Java evaluates an argument before the method runs, so the default was computed and thrown away. That is harmless for a constant such as"not known", and a real cost when the default is a database query or an object that takes time to build.orElseGet(supplier)takes aSupplierinstead, which it calls only when theOptionalis empty: 0 calls with a value present, 1 without. Use it whenever the default costs anything.orElseThrow()(Java 10) throwsNoSuchElementExceptionwith the message “No value present”. The overload with a supplier throws an exception of your choice, with a message that says what was missing. Use them when absence is a bug or a failed request, not a normal case.ifPresentOrElse(action, emptyAction)(Java 9) runs one of two actions and returns nothing: the right tool when the value is only used for a side effect, such as printing.or(supplier)(Java 9) gives anotherOptionalwhen the first is empty, and keeps the result anOptional, so the chain can continue.
get() is the method to avoid. The last command shows why: on an empty Optional it throws, and the stack trace
says no more than “No value present”, exactly like the NullPointerException the Optional was meant to prevent.
Its Javadoc names orElseThrow() as the preferred alternative: it does the same, and its name says that it can throw.
An isPresent() check followed by get() compiles and works, but it is a null check in disguise; map,
orElse… and ifPresent… say the same thing in one call.
Where Optional does not belong
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.stream.IntStream;
/** An Optional in a field: the record compiles, but cannot be serialized. */
record Profile(String name, Optional<String> nickname) implements Serializable {}
class Misplaced {
static void main() throws IOException {
try (ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream())) {
out.writeObject(new Profile("Asha", Optional.of("Ash")));
} catch (NotSerializableException e) {
IO.println("NotSerializableException: " + e.getMessage());
}
try {
Optional<String> wrong = Optional.of(null);
} catch (NullPointerException e) {
IO.println("Optional.of(null): NullPointerException; use Optional.ofNullable");
}
OptionalInt best = IntStream.of(82, 38, 91).max(); // no Integer objects
OptionalInt none = IntStream.empty().max();
IO.println("best " + best.getAsInt() + ", none " + none + ", none.orElse(-1) " + none.orElse(-1));
}
} from session import run
run("""
java Misplaced.java
""") Output
# javac 25.0.4.1 $ java Misplaced.java NotSerializableException: java.util.Optional Optional.of(null): NullPointerException; use Optional.ofNullable best 91, none OptionalInt.empty, none.orElse(-1) -1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 misplaced_session.py
- Fields.
Optionaldoes not implementSerializable, so the recordProfile, which does, fails at run time withNotSerializableException: java.util.Optionalas soon as it holds one. The compiler did not warn. Keep the field a nullableStringand returnOptional.ofNullable(nickname)from the accessor, asUser.addressIfKnown()does for its address. - Parameters. A method that takes an
Optional<String>makes every caller wrap its argument, and theOptionalitself can still benull. Overload the method, or accept a nullable value and document it. - Collections. A
List<Optional<User>>is a list with gaps marked in an awkward way. Leave the missing values out, and return an empty list rather than anOptional<List<…>>. nullitself.Optional.of(null)throws aNullPointerException; useOptional.ofNullable. And a method that returns anOptionalmust never returnnull: the Javadoc says a variable of typeOptionalshould always point to anOptionalinstance.- Identity.
Optionalis a value-based class, so never compare two of them with==or synchronise on one. Compare them withequals, or better, compare the values they hold. - Primitives.
OptionalInt,OptionalLongandOptionalDoublehold a primitive without anIntegerobject.IntStream.max()returns anOptionalInt, read withgetAsInt()ororElse(-1).
Optional in streams
Streams hand out Optionals where a result may be missing, findFirst, max and reduce among them, and
Optional.stream() turns a present value into a one-element stream and an empty one into nothing:
import java.util.List;
import java.util.Optional;
class Found {
static void main() {
Directory directory = new Directory();
List<String> ids = List.of("u1", "u9", "u3", "u7");
// Optional.stream() turns a present value into a one-element stream and an empty one into no elements.
List<String> names = ids.stream()
.map(directory::find) // Stream<Optional<User>>
.flatMap(Optional::stream) // Stream<User>: the empty results drop out
.map(User::name)
.toList();
IO.println("found: " + names);
Optional<String> firstWithCode = ids.stream()
.map(directory::find)
.flatMap(Optional::stream)
.filter(u -> u.addressIfKnown().map(Address::postalCode).isPresent())
.map(User::name)
.findFirst();
IO.println("first with a postal code: " + firstWithCode.orElse("none"));
}
} from session import run
run("""
java Found.java
""") Output
# javac 25.0.4.1 $ java Found.java found: [Asha, Meera] first with a postal code: Asha
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 found_session.py
map(directory::find) gives one Optional<User> per id, two of them empty. flatMap(Optional::stream) keeps the
users that were found and drops the rest, without a single isPresent() check. The second pipeline ends with
findFirst, whose Optional the program unpacks once, with orElse, at the very end.
Your turn: replace null returns with an Optional chain
managerEmailOld returns null when an employee, a manager or an email address is missing, and leaves every caller
to remember it. Write managerEmail(String employee) returning Optional<String>, with the same answers. The checks
compare the two methods for every case, and they count calls of the helpdesk fallback to prove that orElseGet does
not build the default when a value exists.
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
class ManagerEmail {
static final Map<String, String> MANAGER = Map.of("asha", "ravi", "ravi", "meera", "kabir", "zoya");
static final Map<String, String> EMAIL = Map.of("ravi", "Ravi@Example.com", "meera", "meera@example.com");
/** Before: each step may return null, and every caller has to remember that. */
static String managerEmailOld(String employee) {
String manager = MANAGER.get(employee);
if (manager == null) return null;
String email = EMAIL.get(manager);
if (email == null) return null;
return email.toLowerCase(Locale.ROOT);
}
/** After: the return type says that the answer may be missing. */
static Optional<String> managerEmail(String employee) {
return Optional.ofNullable(MANAGER.get(employee))
.map(EMAIL::get) // a manager without an email: null, so empty
.map(email -> email.toLowerCase(Locale.ROOT));
}
static int helpdeskCalls = 0;
static String helpdesk() {
helpdeskCalls++;
return "helpdesk@example.com";
}
static void check(String what, Object got, Object want) {
IO.println((Objects.equals(got, want) ? "ok " : "WRONG ") + what + ": " + got);
}
static void main() {
check("asha's manager", managerEmail("asha"), Optional.of("ravi@example.com"));
check("manager without an email", managerEmail("kabir"), Optional.empty());
check("no such employee", managerEmail("nobody"), Optional.empty());
for (String id : List.of("asha", "ravi", "kabir", "nobody")) {
check("same answer as before for " + id, managerEmail(id).orElse(null), managerEmailOld(id));
}
String to = managerEmail("asha").orElseGet(ManagerEmail::helpdesk);
check("default not computed when present", helpdeskCalls, 0);
to = managerEmail("kabir").orElseGet(ManagerEmail::helpdesk);
check("default computed once when empty", helpdeskCalls + " " + to, "1 helpdesk@example.com");
}
} from session import run
run("""
java ManagerEmail.java
""") Output
# javac 25.0.4.1 $ java ManagerEmail.java ok asha's manager: Optional[ravi@example.com] ok manager without an email: Optional.empty ok no such employee: Optional.empty ok same answer as before for asha: ravi@example.com ok same answer as before for ravi: meera@example.com ok same answer as before for kabir: null ok same answer as before for nobody: null ok default not computed when present: 0 ok default computed once when empty: 1 helpdesk@example.com
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
Map.get returns null for a missing key, so Optional.ofNullable(MANAGER.get(employee)) starts the chain and
map(EMAIL::get) turns a manager without an address into an empty Optional, because map treats null as empty.
The last step needs a lambda, email -> email.toLowerCase(Locale.ROOT), because a method reference cannot pass the
Locale. The two calls with orElseGet show the fallback running 0 times for Asha and once for Kabir.
Interview questions
Fresher · Why is calling get() on an Optional discouraged, and what should you call instead?
get() returns the value when one is present and throws NoSuchElementException with the message “No value present” when the Optional is empty, so calling it without a check brings back the failure Optional was meant to prevent, only with a different exception. The Javadoc names orElseThrow() as the preferred alternative: it behaves the same, and its name warns the reader that it can throw. Better still, avoid unpacking at all: transform with map, flatMap and filter, and finish with orElse for a cheap default, orElseGet for a costly one, orElseThrow with a supplier for a meaningful exception, or ifPresentOrElse when the value is only used for an action. An isPresent() check followed by get() works, but it is a null check in disguise.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A method returns cache.find(key).orElse(loadFromDatabase(key)). Metrics show the database is queried on every call, even for keys that are in the cache. Explain why, fix it, and say how a test would prove the fix.
-
Mid to senior
A code review finds Optional<String> middleName as a field of a class that is serialized into a distributed cache, and service methods that take Optional parameters. What problems do you expect, and what do you recommend instead?
Key takeaways
- Return
Optional<T>when a method may have no result; then chain withmap,flatMapandfilter, which skip their work on an emptyOptional. - Use
flatMapfor a step that itself returns anOptional;mapwould nest it.mapturns anullresult into an emptyOptional. orElsealways evaluates its argument;orElseGetcalls its supplier only when empty. UseorElseGetfor anything costly.- Prefer
orElseThrowtoget(); better still, avoid unpacking until the end of the chain. - Keep
Optionalout of fields, parameters and collections. It is notSerializable, andOptional.of(null)throws. - Use
OptionalInt,OptionalLongandOptionalDoublefor primitives, andOptional.stream()to drop empty results from a stream.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- Optional (Java SE 25) (Oracle)
- OptionalInt (Java SE 25) (Oracle)
- Value-based Classes (Java SE 25) (Oracle)
- Serializable (Java SE 25) (Oracle)
- Stream (Java SE 25): findFirst and flatMap (Oracle)
- Map (Java SE 25): get(Object) (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress