Optional: Xử Lý Thiếu Dữ Liệu An Toàn Thay Thế Con Trỏ Null
Làm chủ Optional trong Java, phân biệt orElse vs orElseGet, chuỗi biến đổi map/flatMap an toàn và các nguyên tắc thiết kế API chống lạm dụng.
TL;DR: Optional<T> là một đối tượng bao bọc (container) thể hiện trạng thái có hoặc không có giá trị, được thiết kế để loại bỏ lỗi kinh điển NullPointerException tại ranh giới trả về của phương thức. Thông qua các hàm chức năng như map, filter, flatMap, bạn có thể xâu chuỗi thao tác xử lý an toàn mà không cần viết các câu lệnh if (x != null) thủ công. Điểm mấu chốt về hiệu năng là phân biệt giữa orElse (đánh giá tham số tức thì - eager) và orElseGet (chỉ đánh giá khi rỗng - lazy qua Supplier). Cuối cùng, Optional chỉ nên dùng cho kiểu trả về của phương thức, không dùng cho thuộc tính lớp (fields), tham số đầu vào hay bọc các Collection.
Sau khi đã làm quen với Lambda và Functional Interface cùng các thao tác trong Stream Basics, bài học này giúp bạn làm chủ công cụ phòng chống lỗi sập ứng dụng phổ biến nhất: Optional.
1. Optional giải quyết sai lầm tỷ đô của con trỏ null như thế nào?
Năm 1965, Tony Hoare phát minh ra con trỏ null và sau này gọi đó là "sai lầm tỷ đô". Trong các hệ thống Java truyền thống, đoạn mã tra cứu thông tin ấn phẩm thư viện rất dễ sụp đổ vì NPE:
// Risk of crash: any null step throws NullPointerException
String borrowerEmail = catalog.findItem("ITEM-01")
.getCurrentBorrower()
.getContactInfo()
.getEmail()
.toLowerCase();
Optional<T> ra đời như một phong bì chứa dữ liệu: Người gọi phương thức buộc phải nhận biết phong bì đó có thể rỗng và được cung cấp công cụ functional để mở phong bì một cách an toàn.

2. Ba phương thức khởi tạo của Optional phân biệt ra sao?
Java cung cấp 3 phương thức xuất xưởng tĩnh (static factories) để tạo một đối tượng Optional:
// 1. Guaranteed non-null (throws NPE immediately if item == null):
Optional<LibraryItem> opt1 = Optional.of(item);
// 2. May be null (safest choice for external inputs):
Optional<LibraryItem> opt2 = Optional.ofNullable(maybeNullItem);
// 3. Explicitly empty:
Optional<LibraryItem> empty = Optional.empty();
[!NOTE] Chỉ dùng
Optional.of()khi bạn kiểm tra hằng số hoặc đối tượng vừa đượcnew. Trong mọi trường hợp dữ liệu lấy từ cơ sở dữ liệu, mạng hoặc tham số bên ngoài, luôn dùngOptional.ofNullable().
3. Các thao tác map, filter và flatMap xử lý an toàn giá trị rỗng thế nào?
Thay vì viết nhiều tầng if-else lồng nhau, Optional cung cấp bộ API tương tự như Stream:
// 1. map(): Bien doi gia tri neu present, bo qua neu empty
Optional<String> title = Optional.ofNullable(item)
.map(LibraryItem::getTitle);
// 2. filter(): Giu lai gia tri neu thoa man dieu kien, nguoc lai bien thanh empty
Optional<LibraryItem> overdueItem = Optional.ofNullable(item)
.filter(LibraryItem::isOverdue);
// 3. flatMap(): Phong hoa khi ham bien doi tra ve mot Optional khac
public Optional<Borrower> findBorrower(LibraryItem item) { ... }
// Neu dung map() se bi long: Optional<Optional<Borrower>>
// Dung flatMap() de duoc: Optional<Borrower>
Optional<Borrower> borrower = Optional.ofNullable(item)
.flatMap(this::findBorrower);
Chuyển đổi sang Stream từ Java 9
Phương thức Optional.stream() giúp tích hợp mượt mà giữa Optional và Stream:
List<String> barcodes = List.of("B01", "B02", "B03");
// Tim tat ca an pham ton tai va bo qua cac ma barcode khong tim thay:
List<LibraryItem> existingItems = barcodes.stream()
.map(catalog::findByBarcode) // Stream<Optional<LibraryItem>>
.flatMap(Optional::stream) // Stream<LibraryItem> (Tu dong bo qua empty)
.toList();
4. Tại sao orElse và orElseGet khác biệt lớn về mặt hiệu năng?
Đây là một trong những lỗi ngầm phổ biến nhất làm suy giảm hiệu năng hệ thống production:
// GIA SU: Cache hit thanh cong, item da ton tai trong cache
Optional<LibraryItem> cached = cache.get("ITEM-01");
// 1. orElse(): LUON LUON thuc thi bieu thuc tham so du da co gia tri!
LibraryItem item1 = cached.orElse(dbService.queryDatabase("ITEM-01"));
// 2. orElseGet(): Chi goi Supplier khi va chi khi Optional bi EMPTY!
LibraryItem item2 = cached.orElseGet(() -> dbService.queryDatabase("ITEM-01"));
orElse(T other)(Eager Evaluation): VìqueryDatabase()là một tham số của hàm trong Java, nó bắt buộc phải được thực thi trước khi truyền vàoorElse, bất kể phong bì có rỗng hay không. Điều này khiến cơ sở dữ liệu phải chịu 100% tải truy vấn ngay cả khi cache hit!orElseGet(Supplier<T> supplier)(Lazy Evaluation): Nhận vào một biểu thức Lambda. HàmqueryDatabase()chỉ được kích hoạt khi cache miss.
[!TIP] Chỉ dùng
orElsecho các giá trị tĩnh có sẵn (như chuỗi hằng số, số nguyên0, danh sách rỗng). Bất kỳ thao tác nào cần tính toán, đọc ổ đĩa, gọi mạng hay cấp phát bộ nhớ nặng đều bắt buộc phải dùngorElseGet.
5. Khi nào nên và không nên sử dụng Optional trong thiết kế phần mềm?
Optional được kiến trúc sư trưởng của Java (Brian Goetz) thiết kế thuần túy làm kiểu trả về cho các phương thức có thể không tìm thấy kết quả.
| Trường hợp sử dụng | Khuyến nghị | Lý do kỹ thuật |
|---|---|---|
| Kiểu trả về của hàm | ✅ NÊN DÙNG (Optional<User> find()) | Bắt buộc người gọi phải đối diện và xử lý trường hợp vắng mặt dữ liệu. |
| Thuộc tính của Class (Field) | ❌ KHÔNG DÙNG (private Optional<X> f) | Tốn thêm 16-24 bytes wrapper trên Heap, không thể Serializable mặc định (lỗi JPA/Hibernate). |
| Tham số của hàm (Parameter) | ❌ KHÔNG DÙNG (void set(Optional<X> p)) | Ép người gọi phải gói dữ liệu rườm rà. Hãy dùng method overloading hoặc @Nullable. |
| Bọc Collection | ❌ KHÔNG DÙNG (Optional<List<X>>) | Tạo ra 2 trạng thái rỗng gây mơ hồ. Hãy trả về List.of() rỗng. |
Cách thiết kế chuẩn cho thuộc tính tùy chọn: Lưu trữ thuộc tính dưới dạng nullable thông thường, nhưng cung cấp getter trả về Optional:
public class LibraryCard {
private String email; // Nullable field, toi uu bo nho
public Optional<String> getEmail() {
return Optional.ofNullable(email); // Tra ve Optional an toan cho caller
}
}
6. Đâu là những cạm bẫy phổ biến khi làm việc với Optional?
❌ Cạm bẫy 1: Gọi trực tiếp .get() mà không kiểm tra
Optional<LibraryItem> opt = catalog.findByBarcode("NOT-FOUND");
LibraryItem item = opt.get(); // Nem NoSuchElementException khi empty!
// Chuan muc:
LibraryItem item = opt.orElseThrow(() -> new NotFoundException("Book not found"));
❌ Cạm bẫy 2: Dùng isPresent() kết hợp get() theo phong cách cũ
// Code smell: Khong khac gi viet if (x != null)
if (opt.isPresent()) {
System.out.println(opt.get().getTitle());
}
// Chuan muc:
opt.ifPresent(item -> System.out.println(item.getTitle()));
❌ Cạm bẫy 3: Trả về null thay vì Optional.empty()
// Anti-pattern: Bien Optional thanh tro cuoi
public Optional<LibraryItem> findItem(String code) {
if (code == null) return null; // Tuyet doi khong bao gio tra ve null!
return Optional.ofNullable(repo.find(code));
}
7. Tóm tắt các điểm cốt lõi
Optionallà container báo hiệu giá trị có thể vắng mặt ở kiểu trả về của phương thức.- Dùng
Optional.ofNullable()cho dữ liệu ngoài, dùngOptional.empty()cho trường hợp rỗng. - Dùng
orElsecho hằng số có sẵn; bắt buộc dùngorElseGetcho các thao tác tốn tài nguyên. - Không dùng
Optionallàm Field, Parameter, hay bọc Collection; thay vào đó hãy trả về Collection rỗng.
8. Câu hỏi tự kiểm tra (Self-Check)
- Q1Tại sao lời gọi cached.orElse(fetchFromDb()) lại gây quá tải cơ sở dữ liệu ngay cả khi dữ liệu đã có sẵn trong cache?
- Q2Khi nào ta nên sử dụng flatMap thay cho map trên một đối tượng Optional?
- Q3Vì sao không nên khai báo thuộc tính lớp (instance field) dưới dạng Optional<T>?
- Q4Tại sao phương thức trả về danh sách không bao giờ nên dùng kiểu Optional<List<T>>?
- Q5Điểm khác biệt quan trọng giữa Optional.of() và Optional.ofNullable() là gì?
Bài tiếp theo: Collectors Deep Dive: Gom Tụ Dữ Liệu Nâng Cao và Custom Collector
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Stream Nâng Cao: flatMap, takeWhile, dropWhile và Stateful Operations
Bài tiếpCollectors Deep Dive: toMap, groupingBy và Custom Collector
Lộ trình Junior Java Backend Developer
Chặng 6/21 · Lambda & Stream API — bài 7/12 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này