استخدام Optional بشكل جيد
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
The previous lesson gave you the box: a method returns Optional<T>, and the type tells the caller that the box may be empty. This lesson is about what to do with the box. You could open it with isPresent() and an if every time, but Optional has methods that let you describe the whole job in one chain: transform the value if it is there, keep it only if it passes a test, and finish with a default or an error if it is not.
Think of the gift box again. map is "if there is a gift, wrap it differently". filter is "keep the gift only if it is the right kind". orElse is "if the box is empty, use this spare gift". orElseThrow is "if the box is empty, complain".
Why this matters for backend work: a typical request handler finds a record, pulls one field out of it, and either returns that field or reports "not found". With Optional that is one chain of three or four calls, and the empty case is handled in one visible place at the end.
This lesson uses lambdas and method references (BEG-035), the functional interfaces Function, Predicate, and Supplier (BEG-035), and exceptions. The streams module (BEG-036) already showed findFirst, which returns an Optional; this lesson shows what to do with that result. Everything was compiled and run with JDK 21.
Learning Objectives
By the end of this lesson you will be able to:
- Transform the value inside an
Optionalwithmap, and predict what happens when it is empty. - Keep a value only if it passes a test with
filter. - Unwrap with
orElse,orElseGet, andorElseThrow, and say how they differ. - Explain when
orElsedoes unnecessary work andorElseGetavoids it. - Run an action only when a value is present with
ifPresentandifPresentOrElse. - Read and write a chain such as
find(...).map(...).filter(...).orElse(...).
Written Explanation
map: transform what is inside
map takes a Function and applies it to the value if there is one. The result is a new Optional holding the transformed value. If the box was empty, map does nothing and returns an empty box.
Optional<String> name = Optional.of(" ada ");
Optional<Integer> length = name.map(String::trim).map(String::length);The type changes with the function: Optional<String> mapped with String::length becomes Optional<Integer>. This is the answer to "what does opt.map(String::length) return?": not an int, but an Optional<Integer>. Per the Javadoc, if the function itself returns null, map gives an empty Optional rather than a box holding null.
Because an empty box stays empty through every map, you can chain several steps and never write an if.
filter: keep only if it passes
filter takes a Predicate. If the value is present and the test is true, you get the same Optional back. If the test is false, or the box was empty, you get an empty Optional.
Optional<String> longName = name.filter(n -> n.length() > 20); // empty for a short nameUnwrapping: orElse, orElseGet, orElseThrow
A chain eventually has to end in a plain value. Three methods do that, and each says what to do with an empty box:
| Method | If a value is present | If empty |
|---|---|---|
orElse(other) | returns the value | returns other |
orElseGet(supplier) | returns the value | calls the Supplier and returns its result |
orElseThrow(supplier) | returns the value | throws the exception the Supplier creates |
Choose by what "nothing found" means in your program. A harmless default (a display name of "guest", an empty text) fits orElse. A missing value that should stop the operation fits orElseThrow.
The supplier form of orElseThrow is a lambda that builds the exception:
Contact c = found.orElseThrow(() -> new NoSuchElementException("No contact 5"));The signature is orElseThrow(Supplier<? extends X>), where X is the exception type. If X is a checked exception, the enclosing method must declare it, exactly as with an ordinary throw. Since Java 10 there is also a no-argument orElseThrow() that throws NoSuchElementException. The Javadoc recommends it over get() as a clearer name.
orElse versus orElseGet
The argument of orElse is an ordinary expression, so Java evaluates it before the call, whether or not the box is empty. The argument of orElseGet is a lambda, and the lambda runs only when the box is empty. The difference is invisible when the default is a constant such as "guest", and matters when computing the default costs something (a calculation, a database call) or has a side effect. Example 2 shows it with real output.
Rule of thumb: constant or already-existing value, use orElse. A method call or new object that is only needed in the empty case, use orElseGet.
Acting on a value: ifPresent and ifPresentOrElse
When the goal is a side effect, such as printing or logging, not a result, pass a Consumer:
ifPresent(consumer)runs it only if a value is present.ifPresentOrElse(consumer, runnable)runs the consumer if present, else the second argument (aRunnable, a lambda with no inputs and no result). It was added in Java 9.
Reading a chain
Read from left to right as a pipeline, much like a stream:
findById(id).map(Contact::email).filter(e -> e.contains("@")).orElse("no email on file");"Find the contact; if there is one, take its email; keep it only if it contains @; otherwise use the text no email on file." The last call decides what an empty box means, and everything before it is written for the present case only.
Examples
Example 1: map, filter, and acting (simple)
import java.util.Optional;
public class Ex1 {
static Optional<String> findName(int id) {
return id == 1 ? Optional.of(" ada lovelace ") : Optional.empty();
}
public static void main(String[] args) {
Optional<String> found = findName(1);
Optional<String> missing = findName(7);
System.out.println(found.map(String::trim));
System.out.println(found.map(String::trim).map(String::length));
System.out.println(missing.map(String::trim));
System.out.println(found.filter(n -> n.trim().length() > 20));
String shown = missing.map(String::toUpperCase).orElse("guest");
System.out.println(shown);
System.out.println(found.map(String::trim).map(String::toUpperCase).orElse("guest"));
found.ifPresent(n -> System.out.println("Hello " + n.trim()));
missing.ifPresent(n -> System.out.println("never printed"));
missing.ifPresentOrElse(
n -> System.out.println("Hello " + n),
() -> System.out.println("Nobody to greet"));
}
}Output:
Optional[ada lovelace]
Optional[12]
Optional.empty
Optional.empty
guest
ADA LOVELACE
Hello ada lovelace
Nobody to greetLine 3 shows map on an empty box, which stays empty. Line 4 shows filter rejecting a 12-letter name against a limit of 20. The "never printed" action really never runs.
Example 2: orElse versus orElseGet (practical)
import java.util.Optional;
public class Ex2 {
static int calls = 0;
static String expensiveDefault() {
calls++;
System.out.println("computing default...");
return "default";
}
public static void main(String[] args) {
Optional<String> present = Optional.of("value");
Optional<String> empty = Optional.empty();
System.out.println(present.orElse(expensiveDefault()));
System.out.println("calls after orElse: " + calls);
System.out.println(present.orElseGet(() -> expensiveDefault()));
System.out.println("calls after orElseGet (present): " + calls);
System.out.println(empty.orElseGet(Ex2::expensiveDefault));
System.out.println("calls after orElseGet (empty): " + calls);
}
}Output:
computing default...
value
calls after orElse: 1
value
calls after orElseGet (present): 1
computing default...
default
calls after orElseGet (empty): 2The box was full on the first call, yet computing default... printed, because orElse evaluated its argument anyway. orElseGet computed nothing until the box was actually empty. The last call also shows a method reference, Ex2::expensiveDefault, in place of the lambda.
Example 3: a lookup chain with a clear failure (realistic)
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
public class Ex3 {
record Contact(String name, String email) { }
static Map<Integer, Contact> store = new LinkedHashMap<>();
static Optional<Contact> findById(int id) {
return Optional.ofNullable(store.get(id));
}
static String emailOf(int id) {
return findById(id)
.map(Contact::email)
.filter(e -> e.contains("@"))
.orElse("no email on file");
}
static Contact require(int id) {
return findById(id).orElseThrow(
() -> new NoSuchElementException("No contact " + id));
}
public static void main(String[] args) {
store.put(1, new Contact("Ada", "ada@example.com"));
store.put(2, new Contact("Grace", "unknown"));
System.out.println(emailOf(1));
System.out.println(emailOf(2));
System.out.println(emailOf(3));
System.out.println(require(1).name());
System.out.println(require(3).name());
}
}Output:
ada@example.com
no email on file
no email on file
Ada
Exception in thread "main" java.util.NoSuchElementException: No contact 3
at Ex3.lambda$require$1(Ex3.java:24)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at Ex3.require(Ex3.java:23)
at Ex3.main(Ex3.java:34)emailOf handles three different cases (no contact, bad email, good email) in one chain. require shows the other policy: the caller needs a contact, and the exception message names the missing id. That message is much more useful in a log than a bare NullPointerException. (Stack-trace line numbers inside the JDK can differ between versions.)
Backend Insight
Two policies cover most backend lookups, and Optional lets you pick one explicitly:
- Default or skip: the data is optional, for example an optional phone number on a profile. Use
map(...).orElse(...). - Fail clearly: the data is required, for example an order that must exist before it can be shipped. Use
orElseThrowwith a message that names what was missing.
In web frameworks you will meet later, a "not found" exception thrown from the service layer is typically translated into a 404 response. That works well when the exception carries the id and the entity name. Frameworks such as Spring Data return Optional from their find-by-id methods, so you will write chains like Example 3 against real repositories.
Also notice how this mirrors streams: findFirst returns an Optional, and the usual way to finish a stream that looks for one element is .findFirst().orElse(...) or .findFirst().orElseThrow(...).
Common Mistakes
Mistake 1: unwrapping with isPresent() and get() when a chain would do
- Incorrect:
if (name.isPresent()) { return name.get().toUpperCase(); } else { return "guest"; } - Correct:
return name.map(String::toUpperCase).orElse("guest");
The long form works, but it is longer, and it keeps a get() in the code, which is easy to separate from its check during later edits.
Mistake 2: expensive work inside orElse
- Incorrect:
found.orElse(loadDefaultFromDisk())(runs the disk read every time) - Correct:
found.orElseGet(() -> loadDefaultFromDisk())
Mistake 3: expecting map to return the plain value
- Incorrect:
int n = opt.map(String::length);(does not compile: it is anOptional<Integer>) - Correct:
int n = opt.map(String::length).orElse(0);
Mistake 4: orElse(null) to get back to the old style
opt.orElse(null) brings the null back, and the caller's NPE risk with it. Use it only at a boundary with older code that expects null, and say so in a comment.
Exercises
Try each exercise yourself before opening the solutions. Solutions were compiled and run with JDK 21.
Level A: Foundation
A1. Match each method to what it does when the Optional is empty: map, filter, orElse, orElseThrow. Options: (1) throws, (2) returns the argument, (3) returns an empty Optional.
أظهر الحل A1
A1. map: (3). filter: (3). orElse: (2). orElseThrow: (1).
A2. Predict the result: Optional.of("kiwi").map(String::length).filter(n -> n > 5).orElse(-1).
أظهر الحل A2
A2. -1. The length is 4, the filter wants more than 5, so the box becomes empty, and orElse gives -1.
A3. Which is better for a default of new ArrayList<>(): orElse or orElseGet? And for the default "N/A"? Why?
أظهر الحل A3
A3. For new ArrayList<>() use orElseGet(ArrayList::new): with orElse, a new list is created every time, even when it is not needed. For the constant "N/A" either works; orElse is simpler because nothing is computed.
Level B: Application
B1. Given Optional<String> email, write a method that returns it trimmed and lower-cased, or "n/a" when empty. Use a chain, not if.
أظهر الحل B1
B1.
static String cleanEmail(Optional<String> email) {
return email.map(String::trim).map(String::toLowerCase).orElse("n/a");
}cleanEmail(Optional.of(" ADA@Example.COM ")) gives ada@example.com; an empty box gives n/a. (This method takes an Optional parameter only to keep the exercise self-contained; the next lesson explains why that is usually discouraged.)
B2. Given Optional<Integer> age, return "adult" for 18 or more, "minor" for less, and "unknown" when empty. Do it with map and orElse.
أظهر الحل B2
B2.
static String ageGroup(Optional<Integer> age) {
return age.map(a -> a >= 18 ? "adult" : "minor").orElse("unknown");
}Ages 17, 30, and empty give minor, adult, and unknown.
B3. Rewrite with a chain:
String result;
if (city.isPresent()) {
result = city.get().toUpperCase();
} else {
result = "UNKNOWN";
}أظهر الحل B3
B3. String result = city.map(String::toUpperCase).orElse("UNKNOWN");
Level C: Challenge
C1. Given record Order(int id, double total) and a List<Order>, write double totalOf(List<Order> orders, int id) using a stream (filter, then findFirst) and Optional. For an unknown id it must throw IllegalArgumentException with the message No order <id>.
أظهر الحل C1
C1.
static double totalOf(List<Order> orders, int id) {
return orders.stream()
.filter(o -> o.id() == id)
.findFirst()
.map(Order::total)
.orElseThrow(() -> new IllegalArgumentException("No order " + id));
}With orders 1 (9.5) and 2 (20), totalOf(orders, 2) gives 20.0, and totalOf(orders, 5) throws IllegalArgumentException: No order 5.
C2. Explain the difference between x.filter(s -> s.length() > 4).map(String::length).orElse(-1) and x.map(String::length).filter(n -> n > 2).orElse(-1) for x = Optional.of("kiwi"). Give both results.
أظهر الحل C2
C2. First chain: the filter runs on the string "kiwi", which has 4 letters, so the box becomes empty and the result is -1. Second chain: map first turns it into 4, then the filter checks 4 > 2, which is true, so the result is 4. The order matters because each step sees the output of the previous step.
Mini Challenge
Write a small UserDirectory with a Map<String, String> of username to email (three entries, one email with the text pending instead of an address). Add:
Optional<String> emailOf(String username)usingofNullable.String contactLine(String username)that returnsEmail: <address>when the user has a real address (contains@), andNo email availableotherwise, usingfilter,map, andorElse.String requireEmail(String username)that throwsIllegalStateExceptionwith a message naming the username when there is no address.
Run all three for an existing user, the pending user, and an unknown user, and paste the real output. Check your work: there is no get(), no isPresent(), and no orElse(null).
Summary & Next Lesson
Key takeaways
maptransforms the value if present and returns anOptional; on an empty box it stays empty.filterkeeps the value only if the test passes.orElse(x)gives a default,orElseGet(supplier)computes the default only when needed, andorElseThrow(supplier)throws your own exception.orElseevaluates its argument every time;orElseGetruns its lambda only when the box is empty.ifPresentandifPresentOrElserun actions instead of producing a value.- A good chain handles the empty case once, at its end.
Next lesson
The next lesson, "Optional anti-patterns", covers the misuses: Optional in fields and parameters, Optional<List<T>>, bare get(), and the wordy isPresent/get pattern.
Self-check
- What does
opt.map(String::length)return? - Which method computes its default only when the box is empty?
- Which method do you use to throw your own exception for an empty box?
- What does
filterreturn when the test fails? - Why can
orElse(expensive())be wasteful?
أظهر الإجابات
- An
Optional<Integer>. orElseGet.orElseThrowwith a supplier of the exception.- An empty
Optional. - The argument
expensive()is evaluated before the call, even when the box already holds a value.
References
- Oracle, Java SE 21 API:
Optional. Supports the behaviour ofmap(includingnullresults),filter,orElse,orElseGet(the supplier runs only when absent),orElseThrow(Supplier)and its no-argument form (Java 10, recommended overget()),ifPresent, andifPresentOrElse(Java 9). - Not verified from documentation: the claim that a thrown
NoSuchElementExceptionis typically turned into a 404 by web frameworks (a general description of common practice; check your framework's documentation) and the statement that Spring Data repositories returnOptionalfrom find-by-id methods (the Spring documentation was not fetched).
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.