Optional trong Java: dùng đúng cách và các anti-pattern
1. Tổng quan
Optional ra đời từ Java 8 để biểu diễn giá trị “có thể không tồn tại” một cách rõ ràng hơn null. Nhưng rất nhiều dev dùng sai: lạm dụng Optional làm field, tham số, hoặc gọi .get() như thể nó không bao giờ rỗng. Bài này giúp bạn hiểu khi nào nên dùng Optional, các pattern đúng, và liệt kê những anti-pattern thường gặp để tránh.
Đọc xong, bạn sẽ biết cách thiết kế API trả về Optional hợp lý, xử lý giá trị rỗng gọn gàng bằng map/filter/orElseGet, và nhận ra ngay những đoạn code “giả Optional” cần refactor.
2. Optional là gì và dùng khi nào
Optional<T> là một container có thể chứa hoặc không chứa giá trị kiểu T. Mục đích chính: làm rõ trong signature phương thức rằng giá trị trả về có thể không tồn tại, buộc caller phải xử lý trường hợp đó thay vì quên check 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 trả về Optional<Customer> thay vì Customer (có thể null). Caller nhìn signature là biết ngay cần xử lý trường hợp không tìm thấy, không cần đọc Javadoc.
Lưu ý:
Optionalđược thiết kế cho giá trị trả về, không phải cho mọi chỗ có thể null trong hệ thống.
3. Các cách tạo Optional
Optional<String> present = Optional.of("Hello"); // giá trị chắc chắn khác null
Optional<String> empty = Optional.empty(); // rỗng
Optional<String> nullable = Optional.ofNullable(null); // có thể null, tự chuyển thành empty
Optional.of(value)némNullPointerExceptionngay nếuvaluelànull— dùng khi bạn chắc chắn giá trị không null.Optional.ofNullable(value)an toàn hơn, phù hợp khi giá trị đến từ nguồn không kiểm soát được (kết quả query, map.get…).Optional.empty()tạo instance rỗng, dùng khi biết trước không có giá trị.
4. Xử lý giá trị bên trong đúng cách
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 chỉ giữ lại giá trị nếu điều kiện đúng, nếu không thì trả về Optional.empty(). map biến đổi giá trị bên trong nếu nó tồn tại, không làm gì nếu rỗng. Chuỗi này tránh hoàn toàn if-else lồng nhau kiểm tra null.
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)ném exception có ý nghĩa khi không tìm thấy — nên dùng ở nơi giá trị là bắt buộc.orElse("Guest")dùng khi giá trị mặc định là hằng số đơn giản, luôn được tính dù Optional có giá trị hay không.orElseGet(supplier)dùng khi giá trị mặc định tốn chi phí tính toán — chỉ gọi supplier khi Optional rỗng.
// Sai: luôn gọi buildDefaultCustomer() dù có giá trị hay không
repository.findById(id).orElse(buildDefaultCustomer());
// Đúng: chỉ gọi khi thực sự cần
repository.findById(id).orElseGet(this::buildDefaultCustomer);
4.3. ifPresent và ifPresentOrElse
public class NotificationService {
public void notifyCustomer(Optional<Customer> customer) {
customer.ifPresentOrElse(
c -> System.out.println("Gửi email tới " + c.email()),
() -> System.out.println("Không có customer để gửi")
);
}
}
ifPresentOrElse (từ Java 9) gộp cả hai nhánh có/không có giá trị trong một lời gọi, thay cho if (optional.isPresent()) {...} else {...} dài dòng.
5. Test hành vi với 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 có hẳn các assertion chuyên cho Optional: isPresent(), isEmpty(), hasValue(...). Dùng chúng thay vì gọi .get() rồi so sánh, code test đọc tự nhiên hơn.
6. Các anti-pattern thường gặp
6.1. Gọi .get() không kiểm tra
// Sai: nếu Optional rỗng sẽ ném NoSuchElementException,
// hoàn toàn triệt tiêu lý do dùng Optional
Customer customer = repository.findById(id).get();
Gọi .get() trực tiếp khiến Optional chẳng khác gì null được bọc thêm một lớp — bạn vẫn có thể crash runtime, nhưng còn tốn thêm một dòng code. Hãy dùng orElseThrow() (có exception rõ nghĩa) hoặc xử lý qua map/ifPresent.
6.2. Dùng Optional làm field trong entity/DTO
// Sai: Optional không Serializable, không nên xuất hiện trong entity JPA
@Entity
public class Customer {
private Optional<String> middleName; // tránh
}
Optional không implement Serializable, không được thiết kế để làm field hay tham số constructor. Nếu middleName có thể null, cứ để nó là String và null — chỉ bọc Optional khi trả về từ getter nếu cần:
// Đúng: field vẫn là String, chỉ bọc Optional ở getter nếu muốn
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
6.3. Dùng Optional làm tham số phương thức
// Sai: buộc caller phải tự bọc Optional.of/ofNullable trước khi gọi
public void updateAddress(Optional<String> newAddress) { ... }
Cách này đẩy sự phức tạp sang caller và không mang lại lợi ích nào so với overload hoặc @Nullable. Nên dùng overloading hoặc truyền String kèm kiểm tra null thông thường:
// Đúng
public void updateAddress(String newAddress) {
this.address = newAddress; // caller tự quyết định truyền null hay không
}
6.4. Kiểm tra isPresent() rồi get() như if-null cũ
// Sai: viết lại null-check kiểu cũ, không tận dụng được gì của Optional
if (repository.findById(id).isPresent()) {
Customer c = repository.findById(id).get(); // gọi query 2 lần!
System.out.println(c.name());
}
Đoạn này vừa dài dòng, vừa gọi findById hai lần nếu nó là một query thật. Thay bằng:
// Đúng
repository.findById(id).ifPresent(c -> System.out.println(c.name()));
6.5. Dùng Optional cho collection
// Sai: collection đã có khái niệm "rỗng" sẵn (empty list), không cần Optional
public Optional<List<Order>> findOrdersByCustomer(Long customerId) { ... }
Trả về Optional<List<Order>> buộc caller xử lý hai lớp rỗng: Optional rỗng và list rỗng — dư thừa. Luôn trả về List rỗng khi không có dữ liệu:
// Đúng
public List<Order> findOrdersByCustomer(Long customerId) {
return orders.getOrDefault(customerId, List.of());
}
6.6. Dùng Optional trong hot path hiệu năng cao
Optional tạo thêm object wrapper, không phải chi phí lớn trong đa số ứng dụng, nhưng trong code chạy hàng triệu lần/giây (ví dụ vòng lặp xử lý stream dữ liệu lớn), việc boxing liên tục có thể ảnh hưởng GC. Trong các trường hợp này, cân nhắc kiểm tra null trực tiếp hoặc sentinel value.
Lưu ý: Đừng premature-optimize — chỉ cân nhắc điều này khi đã profile và thấy
Optionalthực sự là nút nghẽn.
7. Bảng tóm tắt khi nào nên/không nên dùng
| Tình huống | Nên dùng Optional? |
|---|---|
| Giá trị trả về của method, có thể không tồn tại | ✅ Nên |
| Field trong entity/DTO | ❌ Không |
| Tham số của constructor/method | ❌ Không |
| Trả về collection (List, Set, Map) | ❌ Không — trả empty collection |
| Local variable trong method để tránh null check lặp | ⚠️ Tùy — cân nhắc, đôi khi if/else rõ hơn |
8. Kết luận
Optional là công cụ tốt để làm rõ hợp đồng API và tránh NullPointerException ở tầng gọi, nhưng chỉ phát huy giá trị khi dùng đúng vị trí: giá trị trả về của phương thức. Tránh biến nó thành field, tham số, hay lớp bọc cho collection — những chỗ đó Optional chỉ làm code rối hơn mà không giải quyết được gì. Khi viết code mới, hãy tự hỏi: “Optional ở đây giúp caller xử lý rõ ràng hơn, hay chỉ là null được đóng gói lại?” — câu trả lời sẽ cho bạn biết nên giữ hay bỏ nó.