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

أنماط مضادة (anti-patterns) في Optional

  • الدرس 3 من 4
  • 12 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

Optional is easy to learn and easy to misuse. After two lessons you can create one, transform it, and unwrap it. The risk now is spreading it everywhere: in every field, every parameter, every collection. Used that way it adds clutter, and in some cases it brings back the very null problems it was meant to remove.

This lesson is a field guide to the misuses, with a "do" and a "don't" for each. The core idea is simple and comes straight from the Javadoc: Optional is primarily meant as a method return type where there is a clear need to represent "no result". Almost every anti-pattern is a way of using it somewhere else, or of using it carelessly.

Why this matters for backend work: code review in real teams catches these patterns constantly, and some of them cause production failures. If you can spot them, your repository and service code will read cleanly and fail with useful messages.

This lesson builds on the first two lessons of this module, on constructors and overloading from the object-oriented modules, on collections (List), and on NullPointerException and NoSuchElementException. Everything below was compiled and run with JDK 21.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain why Optional should not be used for fields and method parameters, and what to use instead.
  • Explain why a method should return an empty collection, not Optional<List<T>>.
  • Explain why an unconditional get() is the most dangerous Optional call.
  • Replace the isPresent() then get() pattern with map, orElse, or orElseThrow.
  • Explain why an Optional variable must never be null.
  • Review a short class and list its Optional misuses with fixes.

Written Explanation

The rule behind the rules

The Javadoc says Optional is primarily intended for use as a method return type where "no result" is a real possibility and null would be error-prone. It also says a variable of type Optional should never itself be null. The advice on fields, parameters, and collections below is widely used design guidance that follows from that intent; the Javadoc does not list those cases one by one.

Anti-pattern 1: Optional as a field

java
class BadProfile {
    private Optional<String> nickname;   // don't
}

Why it is a problem:

  • It adds an extra object to every instance, only to say "this may be missing". A plain field can say the same thing.
  • The field itself can still be null. Nothing stops a constructor from storing null in it, and then every use of the field breaks.
  • It makes the class awkward to build: every caller wraps and unwraps values.

Do this instead: keep the field as a plain String, document that it may be null, and expose it through a getter that returns Optional. The Optional is created on the way out, which is where it helps the caller.

java
class Profile {
    private final String nickname;                       // may be null
    Optional<String> nickname() { return Optional.ofNullable(nickname); }
}

Anti-pattern 2: Optional as a method parameter

java
void send(String to, Optional<Integer> priority)   // don't

The caller now has three ways to say "no priority": Optional.empty(), null, or an Optional containing something. The compiler accepts null for the Optional parameter without complaint, so the method gets a null where it expects a box. It also forces every caller to write Optional.of(...) around a plain number.

Do this instead: use overloads (two methods with the same name and different parameters), or accept a plain value.

java
void send(String to, Integer priority) { ... }
void send(String to) { send(to, 1); }               // default priority

Anti-pattern 3: Optional around a collection

java
Optional<List<Order>> findOrders(String customer)    // don't

A list already has a way to say "nothing": it is empty. An Optional<List<Order>> has two kinds of "nothing" (an empty box and a box with an empty list) and callers must handle both. Return List<Order> and use an empty list when nothing is found. The caller can loop over it, check isEmpty(), or stream it, with no unwrapping. The same applies to arrays and maps.

Anti-pattern 4: an unconditional get()

java
String nick = findNickname(5).get();                // don't

If the box is empty, get() throws NoSuchElementException: No value present. That is the same crash as before, just with a different name, and the compiler does not complain. This is the most common Optional mistake and the one the lab in this module is designed to remove.

Do this instead, depending on what "missing" means:

  • orElse("default") or orElseGet(...) for a harmless default.
  • orElseThrow(() -> new SomeException("clear message")) when it must exist.
  • ifPresent(...) when you only want to act if it exists.

The no-argument orElseThrow() has the same behaviour as get() but its name says what will happen. The Javadoc recommends it over get().

Anti-pattern 5: isPresent() followed by get()

java
if (r.isPresent()) { return r.get().toUpperCase(); }
return "NOBODY";                                     // works, but wordy

This is safe as long as the check and the get() stay together, but it is longer than needed and it keeps a bare get() in the code. The chain r.map(String::toUpperCase).orElse("NOBODY") says the same in one line. The if form is reasonable when the present branch does something large, such as several statements, but in that case ifPresent or ifPresentOrElse with a lambda is often still tidier.

Anti-pattern 6: a null Optional

java
Optional<String> bad = null;                         // don't
return null;                                         // from a method returning Optional

This defeats the purpose: callers trust the box to exist and do not check it for null. Always use Optional.empty() for "nothing". The Javadoc says it directly.

Quick reference

SituationDon'tDo
Lookup that may find nothingreturn nullreturn Optional<T>
Value that may be missing in a classOptional<T> fieldplain field, Optional getter
Optional input to a methodOptional<T> parameteroverload, or plain value
Many results, maybe noneOptional<List<T>>List<T>, empty if none
Need the valueopt.get() uncheckedorElse, orElseGet, orElseThrow
Transform if presentisPresent then getmap(...).orElse(...)
Nothing to returnreturn nullOptional.empty()

Examples

Example 1: the unchecked get() (simple)

java
import java.util.Optional;

public class Ex1 {
    static Optional<String> findNickname(int id) {
        return Optional.empty();
    }

    public static void main(String[] args) {
        Optional<String> nick = findNickname(5);
        System.out.println("Looked up nickname");
        System.out.println(nick.get().toUpperCase());
    }
}

Output:

text
Looked up nickname
Exception in thread "main" java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.get(Optional.java:143)
	at Ex1.main(Ex1.java:11)

The message No value present does not say which lookup failed or for which id. Compare it with orElseThrow(() -> new NoSuchElementException("No nickname for user " + id)), which carries exactly that.

Example 2: field, parameter, and collection (practical)

java
import java.util.List;
import java.util.Optional;

public class Ex2 {
    static class BadProfile {
        private Optional<String> nickname;       // field typed Optional

        BadProfile(Optional<String> nickname) {   // parameter typed Optional
            this.nickname = nickname;
        }
        Optional<String> nickname() { return nickname; }
    }

    static class Profile {
        private final String nickname;            // may be null, documented

        Profile() { this(null); }
        Profile(String nickname) { this.nickname = nickname; }

        Optional<String> nickname() { return Optional.ofNullable(nickname); }
    }

    static List<String> findTags(String post) {
        return post.equals("p1") ? List.of("java", "optional") : List.of();
    }

    public static void main(String[] args) {
        BadProfile b1 = new BadProfile(Optional.empty());
        BadProfile b2 = new BadProfile(null);        // compiles!
        System.out.println(b1.nickname());
        try {
            System.out.println(b2.nickname().isPresent());
        } catch (NullPointerException e) {
            System.out.println("NPE from a null Optional");
        }

        System.out.println(new Profile().nickname());
        System.out.println(new Profile("ada").nickname());
        System.out.println(findTags("p1") + " " + findTags("zzz") + " " + findTags("zzz").size());
        for (String t : findTags("zzz")) {
            System.out.println("never printed: " + t);
        }
    }
}

Output:

text
Optional.empty
NPE from a null Optional
Optional.empty
Optional[ada]
[java, optional] [] 0

BadProfile accepted null without a compile error, and the NullPointerException appeared only when the field was used: the old problem, back again. Profile takes a plain String (two constructors act as the overloads) and still gives callers an Optional to work with. The last lines show why an empty list needs no unwrapping: the loop simply does not run.

Example 3: tidying a wordy method (realistic)

java
import java.util.Optional;

public class Ex3 {
    static Optional<String> find(int id) {
        return id == 1 ? Optional.of("Ada") : Optional.empty();
    }

    static String wordy(int id) {
        Optional<String> r = find(id);
        if (r.isPresent()) {
            return r.get().toUpperCase();
        }
        return "NOBODY";
    }

    static String tidy(int id) {
        return find(id).map(String::toUpperCase).orElse("NOBODY");
    }

    public static void main(String[] args) {
        System.out.println(wordy(1) + " " + wordy(2));
        System.out.println(tidy(1) + " " + tidy(2));
        Optional<String> bad = null;
        System.out.println(bad.isPresent());
    }
}

Compiled with javac -g, the output is:

text
ADA NOBODY
ADA NOBODY
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.isPresent()" because "bad" is null
	at Ex3.main(Ex3.java:26)

Both versions behave identically, and tidy has no get(). The last lines show that a null Optional crashes on the very first method call. It is the same crash as before, one level up, and the compiler gave no warning.

Backend Insight

In a typical service the layers look like this: a repository finds data, a service applies rules, and a controller turns the result into a response. Optional belongs at the boundary where a lookup can find nothing, mostly the return type of repository methods. Inside the service, convert it quickly into a decision: a default, an exception, or an action.

Two consequences you will see in real code bases:

  • Entity classes (the classes that mirror database rows) normally have plain fields. Frameworks that fill those fields generally work with plain values, but behaviour differs, so check the documentation of the framework you use. The getter, not the field, is where Optional is allowed to appear.
  • Service methods for lists (all orders of a customer, all tags of a post) return empty lists when there is nothing. Callers can loop or stream without a special case, and the web layer can return an empty JSON array.

Code reviewers often flag a bare get() first. Make it a habit: whenever you type .get() on an Optional, stop and choose orElse, orElseGet, orElseThrow, or ifPresent instead.

Common Mistakes

Mistake 1: wrapping everything in Optional "to be safe"

  • Incorrect: Optional<String> name = Optional.of(user.getName()); for a value that is always present.
  • Correct: String name = user.getName();

Optional should represent a real possibility of "no result". If something is always there, a plain value is simpler.

Mistake 2: Optional<Boolean> or Optional<List<T>>

An Optional<Boolean> has three states (true, false, empty), which usually confuses callers. An Optional<List<T>> has two kinds of "nothing". Prefer a plain boolean or an empty list.

Mistake 3: opt.get() right after Optional.of(...)

If you built the Optional yourself a line earlier, you did not need it. Use the value directly.

Mistake 4: using Optional.of where the value may be null

Optional.of(null) throws NullPointerException. When the value comes from a Map.get or an older method that may return null, use ofNullable.

Exercises

Try each exercise yourself before opening the solutions. Solutions were compiled and run with JDK 21 where they contain code.

Level A: Foundation

A1. Name three places where Optional should not be used, and one where it should.

أظهر الحل A1

A1. Not in fields, not as method parameters, not around collections (also not as a null value). It should be the return type of a method whose result may be missing, for example a lookup by id.

A2. What does Optional.empty().get() do? Name the exception and name a clearer alternative to get().

أظهر الحل A2

A2. It throws NoSuchElementException with the message No value present. A clearer alternative is orElseThrow(...) with your own exception, or the no-argument orElseThrow() whose name says what happens.

A3. A method finds the orders of a customer who has none. Should it return null, Optional.empty(), or an empty List? Why?

أظهر الحل A3

A3. An empty List. A list already represents "nothing" by being empty, so callers can loop or check isEmpty() without unwrapping. null would risk an NPE, and Optional.empty() would not even compile for a List return type.

Level B: Application

B1. Fix this class so there is no Optional field or parameter, but callers still receive an Optional for the note:

java
class Account {
    private Optional<String> note;
    Account(String owner, Optional<String> note) { ... }
}
أظهر الحل B1

B1.

java
class Account {
    private final String owner;
    private final String note;                 // may be null
    Account(String owner, String note) { this.owner = owner; this.note = note; }
    Account(String owner) { this(owner, null); }
    Optional<String> note() { return Optional.ofNullable(note); }
}

new Account("ada").note().orElse("-") prints -.

B2. Replace this with a one-line chain:

java
if (price.isPresent()) { return price.get() * 1.2; }
return 0.0;
أظهر الحل B2

B2. return price.map(p -> p * 1.2).orElse(0.0);

B3. Explain what is wrong with static Optional<String> find(int id) { return null; }, and fix it.

أظهر الحل B3

B3. It returns null from a method whose type promises an Optional, so a caller who chains .map(...) crashes with an NPE. Fix: return Optional.empty();.

Level C: Challenge

C1. Review this class and list every misuse, then rewrite it:

java
class Customer {
    private Optional<String> phone;
    Optional<List<String>> emails(Optional<Boolean> verifiedOnly) { ... }
    String phoneOrFail() { return phone.get(); }
}
أظهر الحل C1

C1. Misuses: an Optional field (phone); an Optional<List<String>> return type; an Optional<Boolean> parameter; an unconditional get() in phoneOrFail. Rewrite:

java
class Customer {
    private String phone;                                   // may be null
    private List<String> emailList = List.of();
    Optional<String> phone() { return Optional.ofNullable(phone); }
    List<String> emails(boolean verifiedOnly) { ... }       // empty list when none
    String phoneOrFail() {
        return phone().orElseThrow(() -> new IllegalStateException("phone missing"));
    }
}

C2. A teammate says: "We should use Optional for every nullable field, so the code is consistent." Write a short, polite reply that gives three reasons against it and says where Optional should appear instead.

أظهر الحل C2

C2. A sample reply: "Thanks for the suggestion. I would keep Optional for return values. The Javadoc describes that as its intended use. In fields it adds an object per instance and the field can still be null; as a parameter it forces every caller to wrap values and still accepts null; around a collection it creates two kinds of 'nothing'. A plain nullable field with an Optional getter gives us consistency where it counts, at the API boundary."

Mini Challenge

Take this snippet and fix it so that it follows every rule in this lesson. Keep the behaviour (unknown ids show a clear message) but remove all anti-patterns:

java
class Library {
    private Map<String, Optional<String>> authors = new HashMap<>();
    Optional<String> authorOf(String title) { return authors.get(title); }
    String describe(String title) {
        Optional<String> a = authorOf(title);
        if (a.isPresent()) { return title + " by " + a.get(); }
        return null;
    }
}

Hints: the map should hold plain strings, authorOf should use ofNullable, and describe should return a sentence for unknown titles (not null) using a chain. Compile and run your version with one known and one unknown title and paste the output. Check your work: no Optional in a field or map value, no get(), and no return null.

Summary & Next Lesson

Key takeaways

  • Optional is primarily a return type for "maybe no result". Elsewhere it usually adds clutter.
  • Do not use it for fields, for parameters (use overloads), or around collections (return an empty collection).
  • Never call get() without a check. Prefer orElse, orElseGet, orElseThrow, or ifPresent.
  • isPresent() then get() is wordy: map(...).orElse(...) says the same more clearly.
  • An Optional variable or return value must never be null: use Optional.empty().

Next lesson

The next lesson, "Demo: making findById return Optional", puts everything together: you convert a null-returning findById into one that returns Optional, update its callers, and watch a planned get() mistake fail.

Self-check

  1. Why should a method return an empty list, not Optional<List<T>>?
  2. Which exception does get() throw on an empty Optional?
  3. What should you use instead of an Optional parameter?
  4. Rewrite if (o.isPresent()) return o.get(); return "x"; as one line.
  5. Why is Optional<String> x = null; a problem?
أظهر الإجابات
  1. A list can already represent "nothing" by being empty, so wrapping it creates two kinds of "nothing" for callers to handle.
  2. NoSuchElementException.
  3. Overloaded methods, or a plain value.
  4. return o.orElse("x");
  5. Callers trust the Optional to exist and do not check it, so the first method call on it throws NullPointerException.

References

  • Oracle, Java SE 21 API: Optional. Supports the intended use as a return type, the statement that an Optional variable should never be null, get() throwing NoSuchElementException, and the recommendation of orElseThrow() over get().
  • Not verified from a fetched page: the guidance against Optional fields, parameters, and Optional<List<T>>. It is widely used design advice that follows from the intended use above; the Javadoc does not enumerate those cases, and the Oracle Java Magazine and Oracle technical-article pages I tried to fetch returned an access error. Framework behaviour with Optional fields was not checked.

التطبيق

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

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

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

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