Optional in Java: Using it Correctly and Common Anti-Patterns
1. Overview
Optional was introduced in Java 8 to represent values that “may not exist” more clearly than null. But many developers use it incorrectly: misusing Optional as fields, parameters, or calling .get() as if it’s never empty. This article helps you understand when to use Optional, the right patterns, and lists common anti-patterns to avoid.
After reading, you’ll know how to design APIs that return Optional properly, handle empty values cleanly with map/filter/orElseGet, and spot “pseudo-Optional” code that needs refactoring.
2. What is Optional and when to use it
Optional<T> is a container that may or may not hold a value of type T. Main purpose: make it clear in the method signature that the return value may not exist, forcing the caller to handle that case instead of forgetting to check for null.
public class CustomerRepository {
private final Map<Long, Customer> store = new HashMap<>();
public Optional<Customer> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
}
findById returns Optional<Customer> instead of Customer (which could be null). From the signature alone, the caller knows they need to handle the case where nothing is found, without needing to read Javadoc.
Note:
Optionalis designed for return values, not for every place wherenullis possible in the system.
3. Ways to create Optional
Optional<String> present = Optional.of("Hello"); // value is definitely not null
Optional<String> empty = Optional.empty(); // empty
Optional<String> nullable = Optional.ofNullable(null); // may be null, automatically becomes empty
Optional.of(value)throwsNullPointerExceptionimmediately ifvalueisnull— use when you’re certain the value is not null.Optional.ofNullable(value)is safer and suitable when the value comes from an uncontrolled source (query results,map.get(), etc.).Optional.empty()creates an empty instance, use when you know there’s no value upfront.
4. Handling values inside correctly
4.1. map, filter, flatMap
public record Order(Long id, BigDecimal amount, String couponCode) {}
public class DiscountService {
public Optional<BigDecimal> calculateDiscount(Order order) {
return Optional.ofNullable(order.couponCode())
.filter(code -> code.startsWith("SALE"))
.map(code -> order.amount().multiply(BigDecimal.valueOf(0.1)));
}
}
filter only keeps the value if the condition is true, otherwise returns Optional.empty(). map transforms the value inside if it exists, does nothing if empty. This chain avoids nested if-else checks for null entirely.
4.2. orElse, orElseGet, orElseThrow
public class PaymentService {
private final CustomerRepository repository;
public PaymentService(CustomerRepository repository) {
this.repository = repository;
}
public Customer resolveCustomer(Long id) {
return repository.findById(id)
.orElseThrow(() -> new CustomerNotFoundException(id));
}
public String resolveCustomerName(Long id) {
return repository.findById(id)
.map(Customer::name)
.orElse("Guest");
}
}
orElseThrow(supplier)throws a meaningful exception when nothing is found — use where the value is mandatory.orElse("Guest")use when the default value is a simple constant, always evaluated whetherOptionalhas a value or not.orElseGet(supplier)use when the default value is expensive to compute — only calls the supplier whenOptionalis empty.
// Wrong: always calls buildDefaultCustomer() whether there's a value or not
repository.findById(id).orElse(buildDefaultCustomer());
// Right: only calls when truly needed
repository.findById(id).orElseGet(this::buildDefaultCustomer);
4.3. ifPresent and ifPresentOrElse
public class NotificationService {
public void notifyCustomer(Optional<Customer> customer) {
customer.ifPresentOrElse(
c -> System.out.println("Send email to " + c.email()),
() -> System.out.println("No customer to send to")
);
}
}
ifPresentOrElse (from Java 9) combines both the present and absent branches in a single call, replacing the lengthy if (optional.isPresent()) {...} else {...}.
5. Testing behavior with JUnit 5 + AssertJ
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
class DiscountServiceTest {
private final DiscountService service = new DiscountService();
@Test
void shouldReturnDiscountWhenCouponValid() {
Order order = new Order(1L, BigDecimal.valueOf(100), "SALE10");
Optional<BigDecimal> discount = service.calculateDiscount(order);
assertThat(discount)
.isPresent()
.get()
.isEqualTo(BigDecimal.valueOf(10.0));
}
@Test
void shouldReturnEmptyWhenNoCoupon() {
Order order = new Order(2L, BigDecimal.valueOf(100), null);
Optional<BigDecimal> discount = service.calculateDiscount(order);
assertThat(discount).isEmpty();
}
}
AssertJ has dedicated assertions for Optional: isPresent(), isEmpty(), hasValue(...). Use them instead of calling .get() and comparing; test code reads more naturally.
6. Common anti-patterns
6.1. Calling .get() without checking
// Wrong: if Optional is empty it throws NoSuchElementException,
// completely defeating the purpose of using Optional
Customer customer = repository.findById(id).get();
Calling .get() directly makes Optional no different from null wrapped in another layer — you can still crash at runtime, plus waste an extra line of code. Use orElseThrow() (with meaningful exception) or handle via map/ifPresent.
6.2. Using Optional as a field in entity/DTO
// Wrong: Optional is not Serializable and shouldn't appear in JPA entities
@Entity
public class Customer {
private Optional<String> middleName; // avoid
}
Optional doesn’t implement Serializable and isn’t designed to be a field or constructor parameter. If middleName can be null, leave it as String and null — only wrap Optional when returning from a getter if needed:
// Right: field remains String, only wrap Optional in the getter if you want
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
6.3. Using Optional as a method parameter
// Wrong: forces the caller to wrap Optional.of/ofNullable before calling
public void updateAddress(Optional<String> newAddress) { ... }
This approach pushes complexity to the caller and offers no benefit over overloading or @Nullable. Use overloading or pass String with normal null checking:
// Right
public void updateAddress(String newAddress) {
this.address = newAddress; // caller decides whether to pass null or not
}
6.4. Checking isPresent() then get() like old null-checks
// Wrong: rewriting old-style null checks, not leveraging anything about Optional
if (repository.findById(id).isPresent()) {
Customer c = repository.findById(id).get(); // calls query 2 times!
System.out.println(c.name());
}
This is both verbose and calls findById twice if it’s an actual query. Replace with:
// Right
repository.findById(id).ifPresent(c -> System.out.println(c.name()));
6.5. Using Optional for collections
// Wrong: collections already have the notion of "empty" (empty list), no need for Optional
public Optional<List<Order>> findOrdersByCustomer(Long customerId) { ... }
Returning Optional<List<Order>> forces the caller to handle two levels of emptiness: empty Optional and empty list — redundant. Always return an empty List when there’s no data:
// Right
public List<Order> findOrdersByCustomer(Long customerId) {
return orders.getOrDefault(customerId, List.of());
}
6.6. Using Optional in performance-critical hot paths
Optional creates an additional object wrapper, not a major cost in most applications, but in code that runs millions of times per second (like loops processing large data streams), continuous boxing can impact GC. In such cases, consider checking for null directly or using a sentinel value.
Note: Don’t premature-optimize — only consider this after profiling and seeing that
Optionalis actually the bottleneck.
7. Quick reference: when to use Optional
| Situation | Should Use Optional? |
|---|---|
| Return value of a method, may not exist | ✅ Should |
| Field in entity/DTO | ❌ Should Not |
| Constructor/method parameter | ❌ Should Not |
| Returning collection (List, Set, Map) | ❌ Should Not — return empty collection |
| Local variable in method to avoid repeated null checks | ⚠️ Maybe — consider, sometimes if/else is clearer |
8. Conclusion
Optional is a good tool for clarifying API contracts and preventing NullPointerException at the call site, but only fulfills its purpose when used in the right place: return values of methods. Avoid making it a field, parameter, or wrapper for collections — in those places Optional only makes code messier without solving anything. When writing new code, ask yourself: “Does Optional here help the caller handle things more clearly, or is it just null wrapped up again?” — your answer will tell you whether to keep it or drop it.