انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-037-L02

استخدام Optional بشكل جيد

  • الدرس 2 من 4
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 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 Optional with map, and predict what happens when it is empty.
  • Keep a value only if it passes a test with filter.
  • Unwrap with orElse, orElseGet, and orElseThrow, and say how they differ.
  • Explain when orElse does unnecessary work and orElseGet avoids it.
  • Run an action only when a value is present with ifPresent and ifPresentOrElse.
  • 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.

java
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.

java
Optional<String> longName = name.filter(n -> n.length() > 20);   // empty for a short name

Unwrapping: 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:

MethodIf a value is presentIf empty
orElse(other)returns the valuereturns other
orElseGet(supplier)returns the valuecalls the Supplier and returns its result
orElseThrow(supplier)returns the valuethrows 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:

java
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 (a Runnable, 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:

java
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)

java
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:

text
Optional[ada lovelace]
Optional[12]
Optional.empty
Optional.empty
guest
ADA LOVELACE
Hello ada lovelace
Nobody to greet

Line 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)

java
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:

text
computing default...
value
calls after orElse: 1
value
calls after orElseGet (present): 1
computing default...
default
calls after orElseGet (empty): 2

The 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)

java
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:

text
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 orElseThrow with 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 an Optional<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.

java
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.

java
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:

java
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.

java
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:

  1. Optional<String> emailOf(String username) using ofNullable.
  2. String contactLine(String username) that returns Email: <address> when the user has a real address (contains @), and No email available otherwise, using filter, map, and orElse.
  3. String requireEmail(String username) that throws IllegalStateException with 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

  • map transforms the value if present and returns an Optional; on an empty box it stays empty. filter keeps the value only if the test passes.
  • orElse(x) gives a default, orElseGet(supplier) computes the default only when needed, and orElseThrow(supplier) throws your own exception.
  • orElse evaluates its argument every time; orElseGet runs its lambda only when the box is empty.
  • ifPresent and ifPresentOrElse run 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

  1. What does opt.map(String::length) return?
  2. Which method computes its default only when the box is empty?
  3. Which method do you use to throw your own exception for an empty box?
  4. What does filter return when the test fails?
  5. Why can orElse(expensive()) be wasteful?
أظهر الإجابات
  1. An Optional<Integer>.
  2. orElseGet.
  3. orElseThrow with a supplier of the exception.
  4. An empty Optional.
  5. 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 of map (including null results), filter, orElse, orElseGet (the supplier runs only when absent), orElseThrow(Supplier) and its no-argument form (Java 10, recommended over get()), ifPresent, and ifPresentOrElse (Java 9).
  • Not verified from documentation: the claim that a thrown NoSuchElementException is 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 return Optional from find-by-id methods (the Spring documentation was not fetched).

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.