Optional thường được giới thiệu như cách để tránh NullPointerException. Cách giới thiệu đó sai, và sai theo kiểu tạo ra code còn tệ hơn là không dùng nó. Optional không xoá null khỏi Java, không thể xoá, và cũng chưa bao giờ nhắm tới việc đó. Mọi reference type trong ngôn ngữ vẫn nhận null, một biến Optional vẫn có thể là null, và mỗi Optional bạn tạo ra là thêm một object nằm giữa bạn và giá trị thật.
Thứ nó thực sự sinh ra để giải quyết, từ Java 8, hẹp hơn nhiều nhưng rất hữu ích: một method có return type là Optional tự nói ngay trong signature rằng nó có thể không có gì để trả về, nên người gọi không thể quên trường hợp không tìm thấy, và compiler cũng không cho phép họ đối xử với "không có gì" như một giá trị. Dùng đúng vị trí đó thì nó tuyệt vời. Dùng như một thứ thay thế null nói chung — trong field, trong parameter, trong collection — thì nó chỉ thêm allocation, thêm một lớp gián tiếp và thêm một trạng thái phải suy nghĩ, mà không đổi lại được gì.
![]()
Mọi output, exception message và error của javac được trích dưới đây đều sinh ra từ việc compile và chạy code trên OpenJDK 21.0.6 (arm64). Với những method thuộc release muộn hơn Java 8, release được nêu tên và đã được xác nhận bằng cách compile cùng một lời gọi với javac --release 8, 9, 10 và 11.
Optional là một return type, không phải thứ thay thế cho null
Hãy bắt đầu từ chuyện một signature nói được gì và không nói được gì. Một method khai báo User findById(long id) hứa trả về một User. Không có gì trong signature đó cho người gọi biết rằng một bản ghi không tồn tại sẽ quay về dưới dạng null, nên hai lớp phòng thủ duy nhất là tài liệu không ai đọc và một lần kiểm tra null không ai viết.
User findRaw(long id) { ... } // may return null — you have to know
Optional<User> find(long id) { ... } // may be empty — the type says so
Đó là toàn bộ đóng góp của nó. Optional chuyển một sự thật vốn nằm trong comment Javadoc vào trong type system, nơi compiler bắt buộc thi hành: bạn không gọi được getEmail() trên một Optional<User>, nên bạn phải quyết định điều gì xảy ra khi không có user trước khi code compile được.
Ngoài ra không có gì thay đổi. null vẫn là giá trị hợp lệ của mọi reference type, kể cả của chính Optional. Type này không phải Serializable, không phải Comparable, và thực ra không implement interface nào cả — reflection trên java.util.Optional trong Java 21 báo về không có interface nào và superclass là java.lang.Object. Nó là final, và nó mang annotation nội bộ @ValueBased, đó là lý do javac -Xlint:all cảnh báo "attempt to synchronize on an instance of a value-based class" nếu bạn dùng một Optional làm lock.
⚠️ Nếu chỉ lấy được một ý từ bài này:
Optionalthuộc về đường đi ra khỏi một method.nullbên trong một object thì vẫn hoàn toàn ổn. RắcOptionallên field và parameter để "dẹp null" chỉ làm code dài hơn, chậm hơn và khó đọc hơn, và các phần dưới đây chỉ ra chính xác là tệ ở chỗ nào.
Ba trạng thái, và trạng thái thứ tư phá hỏng tất cả
Một Optional là một container chứa đúng một ô. Hoặc là present — có giá trị bên trong — hoặc là empty. Đó là hai trạng thái, và JDK mô hình hoá chúng bằng một instance dùng chung cho empty và một object mới cho mỗi giá trị present.

Optional<String> present = Optional.of("hoang");
Optional<String> empty = Optional.empty();
System.out.println("present = " + present);
System.out.println("empty = " + empty);
System.out.println("present.isPresent() = " + present.isPresent());
System.out.println("empty.isEmpty() = " + empty.isEmpty());
Optional<String> fromNull = Optional.ofNullable(null);
System.out.println("ofNullable(null) = " + fromNull);
System.out.println("same object as Optional.empty()? " + (fromNull == Optional.<String>empty()));
present = Optional[hoang]
empty = Optional.empty
present.isPresent() = true
empty.isEmpty() = true
ofNullable(null) = Optional.empty
same object as Optional.empty()? true
toString đáng để nhớ, vì đó là thứ bạn sẽ thấy trong log: một Optional present in ra Optional[value], một cái empty in ra Optional.empty. Optional.empty() luôn trả về đúng một instance được cache lại, nên phép so sánh reference ở trên cho true; còn Optional.of("hi") == Optional.of("hi") cho false, vì mỗi lần gọi là một lần allocate.
Optional.of(null) hỏng ngay tại lời gọi, không phải sau đó
Hai factory này khác nhau đúng một điểm, và sự khác biệt đó là cố ý. of gọi Objects.requireNonNull trên argument của nó; ofNullable thì không.
try {
Optional<String> bad = Optional.of(null);
System.out.println("never reached " + bad);
} catch (NullPointerException e) {
System.out.println("Optional.of(null) threw " + e);
}
Optional.of(null) threw java.lang.NullPointerException
Nếu không catch, stack trace chỉ đích danh frame gây ra:
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at java.base/java.util.Optional.of(Optional.java:113)
at OfNullThrows.main(OfNullThrows.java:6)
Đây là tính năng chứ không phải khiếm khuyết. Optional.of(x) là một lời khẳng định rằng x không null, và nó hỏng ngay tại dòng code đưa sai giá trị vào chứ không phải ba frame sau đó. Dùng of khi bạn biết chắc giá trị tồn tại, dùng ofNullable khi bạn đang bọc một thứ có thể là null — thường là kết quả của một API cũ hoặc một lần tra Map.
Một biến Optional mà bản thân nó là null
Đây là trạng thái mà nhiều người tưởng là bất khả thi, và nó chính là lý do khiến cách nói "hết NullPointerException" sụp đổ.
Optional<String> broken = null;
System.out.println(broken.isPresent());
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.isPresent()" because "broken" is null
at NullVar.main(NullVar.java:5)
Cái container cũng chỉ là một reference như mọi reference khác, nên một null nằm ở ô đó sẽ ném exception ngay lời gọi đầu tiên và kéo theo toàn bộ lợi ích của type. Message nêu được tên biến vì class được compile kèm thông tin debug — javac -g, thứ mà IDE và build tool bật mặc định. Compile chính file đó mà bỏ tuỳ chọn này thì JVM in ra because "<local4>" is null. Định dạng message đó là phần helpful NullPointerException đã nói ở khoá cơ bản; nó là công cụ chẩn đoán, không phải cách sửa.
Quy tắc rút ra là tuyệt đối: một biến, một field hay một giá trị trả về kiểu Optional không bao giờ được là null. Khi một method đã trả về được Optional.empty() thì nó không còn lý do nào để trả về null, và bất kỳ đoạn code nào vừa trả Optional vừa trả null đều đã gánh chi phí của type mà vẫn giữ nguyên cái bug type đó sinh ra để dẹp.
Toàn bộ API, nhóm theo mục đích của từng method
Optional khai báo hai mươi public method, trong đó ba cái là equals, hashCode và toString. Mười bảy cái còn lại rơi vào bốn nhóm, và nhóm chúng theo mục đích giúp nhớ API dễ hơn hẳn so với một danh sách xếp theo alphabet.
Tạo Optional
| Lời gọi | Nó làm gì | Khi argument là null |
|---|---|---|
Optional.of(value) | bọc một giá trị bắt buộc không null | ném NullPointerException ngay lập tức |
Optional.ofNullable(value) | bọc một giá trị có thể null | trả về instance empty |
Optional.empty() | instance empty dùng chung | không áp dụng |
Hỏi trạng thái
| Lời gọi | Trả về |
|---|---|
isPresent() | true khi bên trong có giá trị |
isEmpty() | true khi không có gì — thêm ở Java 11 |
Cả hai tồn tại để bạn hỏi được; nhưng thường thì không cái nào là thứ đúng đắn để gọi. Một chain kết thúc bằng orElse hoặc map nói đúng điều đó mà không cần rẽ nhánh, và hai phần tiếp theo giải thích vì sao bản có rẽ nhánh lại tệ hơn.
Lấy giá trị ra
| Lời gọi | Khi empty | Có evaluate argument không |
|---|---|---|
get() | ném NoSuchElementException | không áp dụng |
orElseThrow() | ném NoSuchElementException — thêm ở Java 10 | không áp dụng |
orElse(other) | trả về other | luôn luôn, present hay không |
orElseGet(supplier) | gọi supplier | chỉ khi empty |
orElseThrow(supplier) | ném thứ supplier dựng ra | chỉ khi empty |
Biến đổi
| Lời gọi | Mapper trả về | Kết quả |
|---|---|---|
map(f) | một giá trị bình thường | Optional của giá trị đó, empty nếu f trả về null |
flatMap(f) | một Optional | đúng Optional đó, không lồng nhau |
filter(p) | một boolean | giữ giá trị nếu predicate đúng, ngược lại thành empty |
or(supplier) | một Optional | cái hiện tại nếu present, không thì lấy của supplier — Java 9 |
ifPresent(action) | không gì cả | chạy action chỉ khi present |
ifPresentOrElse(action, empty) | không gì cả | một trong hai nhánh — Java 9 |
stream() | không gì cả | một stream không hoặc một phần tử — Java 9 |
Đây là nhóm biến đổi chạy trên một giá trị cần trim:
Optional<String> name = Optional.of(" Hoang ");
Optional<String> blank = Optional.of(" ");
Optional<String> none = Optional.empty();
System.out.println("map : " + name.map(String::trim).map(String::toUpperCase));
System.out.println("filter hit : " + name.map(String::trim).filter(s -> !s.isEmpty()));
System.out.println("filter miss: " + blank.map(String::trim).filter(s -> !s.isEmpty()));
System.out.println("or : " + none.or(() -> Optional.of("anonymous")));
System.out.println("map on empty: " + none.map(String::toUpperCase));
name.ifPresent(v -> System.out.println("ifPresent : got [" + v.trim() + "]"));
none.ifPresentOrElse(
v -> System.out.println("ifPresentOrElse: value " + v),
() -> System.out.println("ifPresentOrElse: nothing there"));
System.out.println("stream present: " + name.map(String::trim).stream().toList());
System.out.println("stream empty : " + none.stream().toList());
map : Optional[HOANG]
filter hit : Optional[Hoang]
filter miss: Optional.empty
or : Optional[anonymous]
map on empty: Optional.empty
ifPresent : got [Hoang]
ifPresentOrElse: nothing there
stream present: [Hoang]
stream empty : []
Hai chi tiết trong output đó rất đáng nhớ. filter biến một giá trị present nhưng không qua được predicate thành Optional.empty, đó là cách một chuỗi rỗng trở thành "không có tên" chỉ trong một lời gọi. Và map trên một Optional empty trả về empty mà không hề gọi mapper — hành vi ngắn mạch sẽ được chứng minh bằng bộ đếm ở phần sau.
Release nào thêm method nào
| Release | Thêm vào |
|---|---|
| Java 8 | of, ofNullable, empty, get, isPresent, ifPresent, filter, map, flatMap, orElse, orElseGet, orElseThrow(Supplier) |
| Java 9 | or, ifPresentOrElse, stream |
| Java 10 | orElseThrow() không tham số |
| Java 11 | isEmpty |
Bảng đó được dựng bằng cách compile một lời gọi cho mỗi method với javac --release 8, --release 9, --release 10 và --release 11 rồi ghi lại release đầu tiên chấp nhận nó, chứ không phải viết theo trí nhớ. Nó có ý nghĩa thực tế: một codebase còn ghim ở Java 8 thì không có or, không có ifPresentOrElse, không có orElseThrow() không tham số, còn isEmpty — method thiếu vắng rõ ràng nhất trong thiết kế ban đầu — mãi tới Java 11 mới xuất hiện.
orElse so với orElseGet: một trong hai luôn luôn chạy
Đây là cặp method bị hiểu nhầm nhiều nhất trong cả API, và cái sai này gần như vô hình khi review code vì hai dòng đọc lên giống hệt nhau. orElse nhận vào một giá trị, nên Java evaluate argument của nó trước khi lời gọi diễn ra — present hay không cũng vậy. orElseGet nhận vào một Supplier, nên phần thân chỉ chạy nếu Optional hoá ra là empty.
static String expensiveDefault() {
System.out.println(" >> expensiveDefault() ran");
return "DEFAULT";
}
public static void main(String[] args) {
Optional<String> present = Optional.of("cached");
Optional<String> empty = Optional.empty();
System.out.println("present.orElse(expensiveDefault())");
System.out.println(" result = " + present.orElse(expensiveDefault()));
System.out.println("present.orElseGet(EagerDefault::expensiveDefault)");
System.out.println(" result = " + present.orElseGet(EagerDefault::expensiveDefault));
System.out.println("empty.orElse(expensiveDefault())");
System.out.println(" result = " + empty.orElse(expensiveDefault()));
System.out.println("empty.orElseGet(EagerDefault::expensiveDefault)");
System.out.println(" result = " + empty.orElseGet(EagerDefault::expensiveDefault));
}
present.orElse(expensiveDefault())
>> expensiveDefault() ran
result = cached
present.orElseGet(EagerDefault::expensiveDefault)
result = cached
empty.orElse(expensiveDefault())
>> expensiveDefault() ran
result = DEFAULT
empty.orElseGet(EagerDefault::expensiveDefault)
>> expensiveDefault() ran
result = DEFAULT
Hãy đọc lại khối đầu tiên. Optional đang present, kết quả là cached, vậy mà expensiveDefault() vẫn chạy — chạy trước cả lời gọi, vì evaluate một argument nghĩa là như vậy. Bản orElseGet trên đúng cái Optional present đó không in ra gì cả.
Vẫn cái bẫy đó, nhưng với chi phí thật
Một println giúp nhìn thấy cơ chế; một default thật mới cho thấy giá phải trả. Ở đây default là một lần tra cứu tự đếm số lần bị gọi, còn vòng lặp hỏi ba user tồn tại và một user không tồn tại.
static int dbHits = 0;
static String loadDefaultFromDb() {
dbHits++;
return "en-US";
}
static Optional<String> lookupUserLocale(String user) {
return "hoang".equals(user) ? Optional.of("vi-VN") : Optional.empty();
}
dbHits = 0;
for (String u : new String[] { "hoang", "hoang", "hoang", "ghost" }) {
lookupUserLocale(u).orElse(loadDefaultFromDb());
}
System.out.println("db hits with orElse = " + dbHits);
dbHits = 0;
for (String u : new String[] { "hoang", "hoang", "hoang", "ghost" }) {
lookupUserLocale(u).orElseGet(RealOrElse::loadDefaultFromDb);
}
System.out.println("db hits with orElseGet = " + dbHits);
db hits with orElse = 4
db hits with orElseGet = 1
Bốn lần tra thay vì một, cho bốn vòng lặp mà chỉ một vòng thực sự cần default. Nhân chuyện đó lên một request handler thì orElse đã làm tải lên nguồn dữ liệu của default tăng gấp bốn. Tệ hơn nữa, nếu default có side effect — insert một bản ghi, tăng một bộ đếm, gửi một message — thì orElse thực hiện side effect đó mọi lần, kể cả khi giá trị vốn đã có sẵn.
Quy tắc rất máy móc và đáng áp dụng mà không cần suy nghĩ:
orElsechỉ khi default là một hằng số hoặc một giá trị đã tính sẵn:orElse(""),orElse(0),orElse(Collections.emptyList()).orElseGetmỗi khi việc tạo ra default có gọi bất cứ thứ gì.
orElse(null) xứng đáng có một ghi chú riêng: nó hợp lệ, nó compile được, và nó trả về null.
System.out.println("empty.orElse(null) = " + Optional.empty().orElse(null));
empty.orElse(null) = null
Thỉnh thoảng đó là cây cầu trung thực để quay lại một API chấp nhận null, nhưng thường xuyên hơn thì nó là dấu hiệu rằng lẽ ra không nên tạo ra Optional đó ngay từ đầu.
get() là method bạn không nên gọi
get() trả về giá trị nếu có và ném exception nếu không. Exception đó rất đặc trưng và đáng nhận ra ngay từ cái nhìn đầu tiên.
Optional<String> empty = Optional.empty();
try {
empty.get();
} catch (NoSuchElementException e) {
System.out.println("get() -> " + e);
}
try {
empty.orElseThrow();
} catch (NoSuchElementException e) {
System.out.println("orElseThrow() -> " + e);
}
try {
empty.orElseThrow(() -> new IllegalStateException("user 42 not found"));
} catch (IllegalStateException e) {
System.out.println("orElseThrow(s) -> " + e);
}
get() -> java.util.NoSuchElementException: No value present
orElseThrow() -> java.util.NoSuchElementException: No value present
orElseThrow(s) -> java.lang.IllegalStateException: user 42 not found
Hai dòng đầu giống hệt nhau vì từ Java 10 chúng là cùng một phần thân method mang hai cái tên. orElseThrow() là cách viết được khuyến nghị: get() đọc lên như một accessor an toàn nhưng hành xử như một lời khẳng định, còn orElseThrow() nói thẳng ra rằng nó có thể ném exception. get() không bị deprecated trong Java 21 — Optional.class.getMethod("get").isAnnotationPresent(Deprecated.class) cho false, và javac -Xlint:all không nói gì về một lời gọi tới nó — nhưng tài liệu của chính JDK giờ hướng bạn sang các lựa chọn khác.
Dạng nhận supplier mới là thứ đáng dùng trong code ứng dụng, vì NoSuchElementException: No value present không nói cho người đọc log biết bất cứ điều gì về lần tra cứu nào đã hỏng.
isPresent() rồi get() chỉ là kiểm tra null với nhiều cú pháp hơn
Đoạn dưới đây là thứ người ta viết nhiều nhất sau khi bắt đầu dùng Optional, và cũng chính là thứ khiến nhiều team kết luận rằng type này không đáng — kết luận đúng, vì ở dạng này thì đúng là không đáng thật.
// a null check
User u = findRaw(id);
String a = (u != null) ? u.email() : "unknown";
// the same null check, with more syntax
Optional<User> o = find(id);
String b = o.isPresent() ? o.get().email() : "unknown";
// what Optional is actually for
String c = find(id).map(User::email).orElse("unknown");
Cả ba đều cho unknown với một id không tồn tại. Bản ở giữa có đúng cái rẽ nhánh của bản đầu, đúng khả năng viết sai như bản đầu, cộng thêm một allocation và một type xa lạ — nó tệ hơn hẳn cái kiểm tra null mà nó thay thế. Bản thứ ba thì không có nhánh nào để viết sai.
Trường hợp còn lại của isPresent() là khi hai nhánh thực sự làm hai việc khác nhau và không nhánh nào tạo ra giá trị, mà ngay cả khi đó ifPresentOrElse thường vẫn đọc dễ hơn. Nếu code của bạn 80% là isPresent() và get() thì bạn đã lấy cú pháp của Optional mà không lấy được chút lợi ích nào.
Chaining: map, flatMap, filter và or
Chaining là chỗ type này thôi làm nghi thức và bắt đầu thực sự bớt code. Mọi method biến đổi đều trả về một Optional khác, và mọi method trong số đó đều không làm gì khi receiver là empty.

map khi mapper trả về giá trị, flatMap khi mapper trả về Optional
Một câu đó là toàn bộ quy tắc, và chọn sai thì được compile error chứ không phải bug lúc runtime — tức là kết cục tốt.
map bọc lại bất cứ thứ gì mapper tạo ra: đưa cho nó một mapper trả về Customer thì bạn nhận Optional<Customer>. flatMap giả định mapper đã tự bọc kết quả rồi, nên nó trả thẳng cái Optional bên trong ra thay vì lồng thêm một lớp. Vậy nên khi một getter tự nó đã trả về Optional — đúng thứ bài này khuyên bạn viết — thì map sinh ra Optional<Optional<Customer>>:
Optional<Optional<Address>> nested =
Optional.of(full).map(Order::rawCustomer).map(Customer::address);
System.out.println("map twice -> " + nested);
System.out.println("flatMap twice -> " +
Optional.of(full).flatMap(Order::customer).flatMap(Customer::address));
map twice -> Optional[Optional[Address[city=Da Nang, zip=550000]]]
flatMap twice -> Optional[Address[city=Da Nang, zip=550000]]
Đi tiếp chain sau một map lẽ ra phải là flatMap thì javac chặn bạn lại:
return Optional.ofNullable(order)
.map(Order::customer) // Optional<Optional<Customer>>
.map(Customer::address) // does not compile
.map(Address::zip)
.orElse("UNKNOWN");
MapMisuse.java:18: error: incompatible types: invalid method reference
.map(Customer::address) // does not compile
^
method address in class Customer cannot be applied to given types
required: no arguments
found: Optional<Customer>
reason: actual and formal argument lists differ in length
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
Cái sai ngược lại — flatMap với một mapper trả về giá trị bình thường — cũng bị bắt ồn ào không kém, và message ghi rõ type mà nó cần:
FlatMisuse.java:10: error: method flatMap in class Optional<T> cannot be applied to given types;
Optional<Customer> c = opt.flatMap(Order::rawCustomer);
^
required: Function<? super Order,? extends Optional<? extends U>>
found: Order::rawCustomer
reason: cannot infer type-variable(s) U
(argument mismatch; bad return type in method reference
Customer cannot be converted to Optional<? extends U>)
Bốn lần kiểm tra null gộp thành một biểu thức
Lấy một model trong đó một order có thể không có customer, một customer có thể không có address, và một address có thể không có mã bưu chính — hình dạng mà hệ thống thật nào rồi cũng lớn lên thành.
static String zipNullChecks(Order order) {
if (order == null) return "UNKNOWN";
Customer c = order.rawCustomer();
if (c == null) return "UNKNOWN";
Address a = c.rawAddress();
if (a == null) return "UNKNOWN";
String zip = a.zip();
if (zip == null) return "UNKNOWN";
return zip;
}
static String zipChained(Order order) {
return Optional.ofNullable(order)
.flatMap(Order::customer)
.flatMap(Customer::address)
.map(Address::zip)
.orElse("UNKNOWN");
}
Cả hai trả về 550000 với một order đầy đủ và UNKNOWN với mọi order thiếu dữ liệu, kể cả một order null. Bản chained có một lối ra thay vì năm và không có cách nào bỏ sót một tầng, và thêm một tầng vào model thì nó dài thêm một dòng chứ không phải ba.
Trace từng stage cho thấy vì sao: một khi một stage sinh ra empty thì mọi stage sau đó cũng sinh ra empty.
== full
1 ofNullable(order) Optional[order]
2 flatMap(Order::customer) Optional[customer]
3 flatMap(Cust::address) Optional[address]
4 map(Address::zip) Optional[550000]
5 orElse("UNKNOWN") "550000"
== customer is null
1 ofNullable(order) Optional[order]
2 flatMap(Order::customer) Optional.empty
3 flatMap(Cust::address) Optional.empty
4 map(Address::zip) Optional.empty
5 orElse("UNKNOWN") "UNKNOWN"
"Sinh ra empty" không đồng nghĩa với "chạy rồi sinh ra empty". Mapper của các stage sau không hề được gọi, một bộ đếm chứng minh điều đó:
AtomicInteger calls = new AtomicInteger();
calls.set(0);
present.map(s -> { calls.incrementAndGet(); return s.toUpperCase(); })
.filter(s -> { calls.incrementAndGet(); return true; })
.map(s -> { calls.incrementAndGet(); return s + "!"; });
System.out.println("present: mapper calls = " + calls.get());
calls.set(0);
empty.map(s -> { calls.incrementAndGet(); return s.toUpperCase(); })
.filter(s -> { calls.incrementAndGet(); return true; })
.map(s -> { calls.incrementAndGet(); return s + "!"; });
System.out.println("empty: mapper calls = " + calls.get());
present: mapper calls = 3
empty: mapper calls = 0
Đó là thứ làm cho một chain dài trở nên an toàn: không lambda nào trong đó nhìn thấy null, và không cái nào chạy trên nhánh không tìm thấy.
filter, or và các method gây side effect
filter thu hẹp một giá trị present thành empty khi predicate không đúng, đó là cách validation tham gia vào chain mà không cần if. or cung cấp hẳn một Optional thay thế và, giống orElseGet, nó lazy — các supplier đứng sau lần trúng đầu tiên không bao giờ được gọi:
Optional<String> resolved = Optional.<String>empty()
.or(() -> Optional.empty())
.or(() -> Optional.of("from config"))
.or(() -> Optional.of("never reached"));
System.out.println(resolved);
Optional[from config]
Một lưu ý về map: vì nó bọc kết quả bằng ofNullable, một mapper trả về null sẽ cho bạn empty chứ không phải Optional[null].
System.out.println("map(s -> null) = " + Optional.of("x").map(s -> null));
map(s -> null) = Optional.empty
Tiện lợi khi bạn đang map qua một getter cũ, nhưng lại là một sự vắng mặt im lặng khi cái null kia thực ra là bug — nên rất đáng biết bạn đang dựa vào cái nào trong hai.
Những chỗ Optional làm code tệ đi
Mọi thứ ở trên là lập luận ủng hộ Optional. Phần này là lập luận phản đối việc dùng nó ở bất cứ đâu ngoài return type, và mỗi mục là một chi phí thật chứ không phải chuyện gu code.

Optional làm field
Điều người ta hay khẳng định là Optional không phải Serializable. Chuyện đó đáng kiểm tra chứ không đáng nhắc lại, nên đây là phép kiểm tra và kết quả của việc thực sự thử serialize:
static class User implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final Optional<String> nickname; // Optional as a field
User(String name, String nickname) {
this.name = name;
this.nickname = Optional.ofNullable(nickname);
}
}
System.out.println("Optional implements Serializable? "
+ Serializable.class.isAssignableFrom(Optional.class));
User u = new User("hoang", "hoangth");
try (ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream())) {
out.writeObject(u);
System.out.println("serialized fine");
} catch (NotSerializableException e) {
System.out.println("writeObject threw " + e);
}
Optional implements Serializable? false
writeObject threw java.io.NotSerializableException: java.util.Optional
Class compile được, object dựng được, và lỗi chỉ đến lúc runtime ngay lần đầu có thứ gì đó muốn ghi nó ra — một session store, một cache, một lời gọi từ xa, một phép deep copy kiểu cũ. Serialization còn là nửa nhỏ hơn của lập luận. Một Optional field tốn thêm một object cho mỗi instance chỉ để mang một sự thật mà bản thân field đã mang được, nó không thể là primitive, và những framework gán field bằng reflection sẽ vui vẻ để nó null, khôi phục đúng cái kiểu hỏng mà bạn dùng type này để tránh.
Hãy để field nhận null và đặt Optional lên accessor, đúng cái biên mà type này sinh ra để phục vụ:
static class User implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final String nickname; // nullable field
User(String name, String nickname) { this.name = name; this.nickname = nickname; }
Optional<String> nickname() { return Optional.ofNullable(nickname); }
}
Ghi một User không có nickname qua một ObjectOutputStream rồi đọc lại:
serialized 90 bytes
nickname() = Optional.empty
Object serialize được, mà mọi người gọi vẫn nhận được một signature thừa nhận rằng nickname có thể vắng mặt.
Optional làm parameter của method
Một parameter kiểu reference có hai trường hợp người gọi phải xử lý: có giá trị, hoặc null. Khai báo nó là Optional làm thành ba, vì bản thân argument cũng có thể là null.
static String greetBad(String name, Optional<String> title) {
return title.map(t -> t + " " + name).orElse(name);
}
System.out.println(greetBad("Hoang", Optional.of("Dr.")));
System.out.println(greetBad("Hoang", Optional.empty()));
System.out.println(greetBad("Hoang", null)); // the third case
Dr. Hoang
Hoang
greetBad(name, null) -> Cannot invoke "java.util.Optional.map(java.util.function.Function)" because "title" is null
Bạn không dẹp được kiểu hỏng nào cả, bạn thêm vào một kiểu hỏng mới — và làm mọi call site ồn ào hơn, vì người gọi đang có sẵn một giá trị bình thường giờ phải viết Optional.of(title) ngay tại chỗ gọi. Hai overload nói đúng điều đó mà không cần wrapper và không sinh ra trường hợp thứ ba:
static String greet(String name) { return name; }
static String greet(String name, String title) { return title + " " + name; }
Hoang
Dr. Hoang
Mỗi overload đều toàn phần: không có giá trị argument nào làm cho một trong hai chạy sai.
Optional nằm trong collection
Một Map vốn đã có câu trả lời đầy đủ cho một key không tồn tại, và bọc value type lại chỉ cho bạn hai kiểu "không có gì" khác nhau mà cùng mang một nghĩa.
Map<String, Optional<String>> bad = new HashMap<>();
bad.put("hoang", Optional.of("hth"));
bad.put("linh", Optional.empty());
System.out.println("bad.get(\"linh\") = " + bad.get("linh"));
System.out.println("bad.get(\"absent\") = " + bad.get("absent"));
Map<String, String> good = new HashMap<>();
good.put("hoang", "hth");
System.out.println("Optional.ofNullable(good.get(\"absent\")) = "
+ Optional.ofNullable(good.get("absent")));
bad.get("linh") = Optional.empty
bad.get("absent") = null
Optional.ofNullable(good.get("absent")) = Optional.empty
Cái map có value là Optional bây giờ phân biệt "có key nhưng không có giá trị" với "không có key", và mọi người đọc code đều phải tự suy ra xem sự phân biệt đó có ý nghĩa gì trong đúng cái map này không — thường là không. Nó cũng allocate thêm một Optional cho mỗi entry. Map thường cộng một lần ofNullable lúc tra cho ra cùng kết quả với chỉ một kiểu vắng mặt. Lập luận đó cũng loại luôn List<Optional<T>>: một list các giá trị có-thể-không-có gần như luôn là một list mà lẽ ra phải lọc bỏ các phần tử không có ngay từ đầu.
Trả về null từ một method có return type là Optional
Đây là tổ hợp tệ nhất có thể có, vì nó là trường hợp duy nhất tệ hơn hẳn so với việc không làm gì cả.
static Optional<String> findNickname(String user) {
return null;
}
try {
System.out.println(findNickname("hoang").orElse("none"));
} catch (NullPointerException e) {
System.out.println("caller NPE: " + e.getMessage());
}
caller NPE: Cannot invoke "java.util.Optional.orElse(Object)" because the return value of "Misuse.findNickname(String)" is null
Người gọi đọc signature, tin nó, chain lên kết quả, và nhận đúng cái exception mà type này sinh ra để ngăn — trong khi vẫn trả tiền cho wrapper. Message của JVM ít nhất cũng chính xác về thủ phạm: nó nêu tên method có giá trị trả về là null. Một method có return type là Optional chỉ có đúng hai lệnh return hợp lệ, Optional.of(...)/Optional.ofNullable(...) và Optional.empty(), còn return null; trong thân method như vậy nên bị coi là lỗi ngay khi review dù javac chấp nhận nó.
Optional cho một giá trị không bao giờ vắng mặt
Mục cuối cùng không có exception nào để trưng ra, và đó chính là lý do nó sống dai trong một codebase. Bọc một thứ luôn luôn có mặt thì tốn một allocation mỗi lần gọi và bắt mọi người gọi phải mở ra một giá trị chưa từng thiếu:
Optional<Long> version() {
return Optional.of(this.version); // never absent
}
Optional.of(x) có allocate: hai lời gọi với cùng nội dung là hai object khác nhau, đúng như việc Optional.of("hi") == Optional.of("hi") cho false. Chỉ Optional.empty() là dùng chung. Một object nhỏ, sống ngắn cho mỗi lời gọi có đáng lo hay không thì hoàn toàn phụ thuộc vào tần suất gọi và không đáng đo ở đây — nhưng cũng chẳng có mặt lợi nào để đem ra cân, vì người gọi không học được gì từ một type nói "có thể không có" về một thứ luôn luôn có. Nếu giá trị là bắt buộc thì cứ trả về nó. Optional trên một field hay một return type không thể empty chỉ là nhiễu, và tệ hơn, nó dạy người đọc thôi tin vào type ngay cả khi type đó thực sự có ý nghĩa.
Những chỗ Optional thực sự thuộc về
Ba hình dạng bao trọn gần như mọi cách dùng tốt, và cả ba thực chất là cùng một hình dạng: một method trả lời một câu hỏi mà "không có gì" là câu trả lời bình thường chứ không phải một lỗi.
/* 1. A repository lookup: "not found" is a normal answer. */
Optional<User> findById(long id) {
return Optional.ofNullable(rows.get(id));
}
/* 2. A parse that can fail, without using an exception for control flow. */
static Optional<Integer> parseInt(String s) {
try {
return Optional.of(Integer.valueOf(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
/* 3. A search with no match. */
static Optional<String> firstLongerThan(List<String> words, int n) {
return words.stream().filter(w -> w.length() > n).findFirst();
}
hoang@example.com
no such user
parse 42 : Optional[42]
parse abc : Optional.empty
parse abc with default: -1
first > 2 : Optional[ccc]
first > 9 : Optional.empty
Tại call site, signature làm hết phần việc. Người gọi muốn một default thì viết orElse, người gọi mà với họ việc không tìm thấy đúng là lỗi thì viết orElseThrow kèm một message chỉ rõ lần tra cứu nào, và không ai vô tình dùng được một giá trị không tồn tại như thể nó tồn tại:
String email = repo.findById(id)
.map(User::email)
.orElseThrow(() -> new NoSuchElementException("user " + id));
Exception in thread "main" java.util.NoSuchElementException: user 99
at GoodUses.lambda$main$1(GoodUses.java:50)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at GoodUses.main(GoodUses.java:50)
Optional.stream() là cây cầu khi cả một lô những lần tra cứu như vậy đổ vào một pipeline: nó biến mỗi kết quả thành không hoặc một phần tử, nên các lần không tìm thấy biến mất mà không cần cặp filter cộng get.
List<String> found = ids.stream()
.map(OptStream::findById)
.flatMap(Optional::stream)
.map(User::email)
.sorted()
.toList();
[hoang@example.com, linh@example.com]
asked for 4, found 2
Chính JDK dùng Optional ở đâu, và vì sao Map.get vẫn trả về null
Thư viện chuẩn là một chỉ dẫn tốt về phạm vi mà type này nhắm tới, vì nó chỉ dùng Optional ở những chỗ đang thêm hẳn một method mới. Reflection trên các interface trong Java 21 cho thấy java.util.stream.Stream khai báo đúng năm method trả về Optional — findFirst, findAny, max, min và reduce — còn java.util.Map khai báo không cái nào.
Stream (5): [findAny -> Optional, findFirst -> Optional, max -> Optional, min -> Optional, reduce -> Optional]
Map (0): []
IntStream (6): [average -> OptionalDouble, findAny -> OptionalInt, findFirst -> OptionalInt, max -> OptionalInt, min -> OptionalInt, reduce -> OptionalInt]
Map.get on a missing key: null
Quy luật ở đây rất rõ. Stream ra đời ở Java 8 cùng lúc với Optional, nên các terminal operation có thể không tìm thấy gì được tự do dùng nó. Map đã tồn tại từ Java 1.2 và Map.get được đặc tả là trả về null cho một key không tồn tại. Đổi return type đó là bất khả thi theo cả hai hướng: nó phá vỡ source compatibility với mọi lời gọi get từng được viết, phá vỡ binary compatibility vì return type là một phần của method descriptor trong file class, và phá vỡ mọi class implement Map nằm ngoài JDK. Nó cũng sẽ huỷ hoại hành vi được ghi trong tài liệu của HashMap là cho phép value là null, mà đó chính là lý do getOrDefault và containsKey tồn tại. Map.get trả về null không phải một sơ suất JDK hối tiếc; đó là một hợp đồng nó không thể thay đổi.
Các biến thể cho primitive trong output đó cũng đáng một dòng. OptionalInt, OptionalLong và OptionalDouble tồn tại chỉ để tránh boxing trong stream API, chúng không có map hay flatMap, và chúng không phải type đa dụng để bạn lấy ra dùng trong code của mình.
Quy tắc thực dụng
- Hãy trả về
Optional. Đừng lưu nó trong field, đừng nhận nó làm parameter, đừng đặt nó vào collection. - Một giá trị kiểu
Optionalkhông bao giờ lànull.return null;từ một method trả vềOptionallà bug chứ không phải lối tắt. Optional.ofkhi giá trị bắt buộc phải có,Optional.ofNullablekhi bọc một thứ có thể không có.orElsechỉ dành cho hằng số. Bất cứ thứ gì có gọi hàm thì đưa vàoorElseGet.- Ưu tiên
orElseThrow()hơnget(), và ưu tiênorElseThrow(supplier)hơn cả hai, để chỗ hỏng nêu được tên lần tra cứu. isPresent()cộngget()chỉ là kiểm tra null khoác áo mới. Hãy nghĩ tớimap,filter,orElsevàifPresentOrElsetrước.flatMapđúng khi mapper tự nó trả về mộtOptional; còn lại thìmap. Chọn sai là compile error chứ không phải bất ngờ lúc runtime.- Đừng bọc một giá trị luôn có mặt, và đừng bọc một collection — hãy trả về collection rỗng.
FAQ
Optional có xoá bỏ NullPointerException khỏi Java không?
Không. null vẫn là giá trị hợp lệ của mọi reference type, một biến Optional vẫn có thể là null rồi ném exception ngay lời gọi đầu tiên, và Optional.of(null) ném exception ngay lập tức theo đúng thiết kế. Thứ Optional làm được là đưa khả năng vắng mặt vào return type của một method, để compiler ép người gọi xử lý nó. Việc đó dẹp được đúng một nhóm bug — quên rằng một lần tra cứu có thể không tìm thấy — và không dẹp thêm gì khác.
orElse và orElseGet khác nhau ở đâu?
orElse nhận một giá trị, nên argument của nó được evaluate trước khi lời gọi chạy, bất kể Optional có present hay không. orElseGet nhận một Supplier và chỉ gọi supplier khi Optional empty. Trong một vòng lặp bốn lần mà chỉ một lần cần default, bản orElse chạm vào lần tra cứu dự phòng bốn lần còn bản orElseGet chạm một lần. Dùng orElse cho hằng số và orElseGet cho mọi thứ có tính toán, có allocate hoặc có side effect.
Khi nào nên dùng flatMap thay cho map?
Đúng khi mapper tự nó trả về một Optional. map bọc lại bất cứ thứ gì mapper tạo ra, nên một getter trả về Optional mà đưa vào map sẽ cho bạn Optional[Optional[...]]; flatMap trả cái bên trong ra đã được mở. Vì một model thiết kế tốt sẽ phơi ra các getter trả về Optional, một chain đi xuyên các object lồng nhau thường là một loạt flatMap kết thúc bằng đúng một map. Chọn sai thì không compile được, nên compiler đang làm giúp bạn phép kiểm tra này.
Gọi get() trên một Optional có sai không?
get() không bị deprecated trong Java 21 và không sinh ra cảnh báo nào từ compiler, nhưng cũng không có lý do gì để chọn nó. Từ Java 10, orElseThrow() là cùng một method dưới một cái tên rõ nghĩa hơn, và cả hai đều ném java.util.NoSuchElementException: No value present khi empty. Một get() được bọc bởi isPresent() là một lần kiểm tra null với thêm cú pháp và thêm một allocation — cùng cái nhánh, cùng rủi ro, không được lợi gì. Hãy dùng map/orElse khi cần một giá trị, hoặc orElseThrow(supplier) khi việc vắng mặt thực sự là lỗi và bạn muốn message nêu tên lần tra cứu.
Một Optional field có serialize được không?
Không. Serializable.class.isAssignableFrom(Optional.class) trả về false — Optional không implement interface nào cả — và ghi một object có Optional field qua ObjectOutputStream sẽ hỏng lúc runtime với java.io.NotSerializableException: java.util.Optional. Cách sửa là để field nhận null và trả về Optional.ofNullable(field) từ accessor; khi đó cùng object đó serialize sạch sẽ mà người gọi vẫn nhìn thấy khả năng vắng mặt trong signature.
Một method có return type là Optional có bao giờ được trả về null không?
Không bao giờ. Đó là cách dùng duy nhất tệ hơn hẳn so với việc không dùng Optional, vì người gọi tin vào signature và chain thẳng lên kết quả. Message của JVM nói rõ chuyện gì đã xảy ra — Cannot invoke "java.util.Optional.orElse(Object)" because the return value of "Misuse.findNickname(String)" is null — nhưng tới lúc đó thì bạn đã trả tiền cho wrapper mà vẫn giữ nguyên bug. Một method trả về Optional chỉ có đúng hai lệnh return hợp lệ: một giá trị đã bọc, hoặc Optional.empty().
Vì sao Map.get vẫn trả về null thay vì Optional?
Vì nó không thể đổi. Map đã nằm trong JDK từ Java 1.2 và Map.get được đặc tả là trả về null cho một key không tồn tại. Đổi return type sẽ phá vỡ mọi lời gọi hiện có ở mức source, phá vỡ binary compatibility vì return type là một phần của method descriptor trong file class, và phá vỡ mọi class bên thứ ba implement Map. Nó cũng đụng độ với việc HashMap cho phép value là null, và đó là lý do getOrDefault và containsKey tồn tại. Hãy tự bọc tại call site bằng Optional.ofNullable(map.get(key)) khi bạn muốn chain.
Optional.of(null) có giống Optional.ofNullable(null) không?
Không, và sự khác biệt là cố ý. Optional.of(null) gọi Objects.requireNonNull và ném NullPointerException ngay tại dòng đó, với java.util.Optional.of(Optional.java:113) trong trace. Optional.ofNullable(null) trả về instance empty dùng chung. Dùng of như một lời khẳng định rằng giá trị tồn tại, để một giá trị sai hỏng ngay chỗ nó được tạo ra; dùng ofNullable khi bạn cố ý bọc một thứ có thể vắng mặt một cách hợp lệ, chẳng hạn một lần tra Map hoặc một getter cũ.
Kết luận
Optional là một container một ô với hai trạng thái và một nhiệm vụ: cho phép một return type thừa nhận rằng nó có thể không có gì để trả về. Mọi thứ tốt đẹp ở nó đều xuất phát từ nhiệm vụ đó — compiler ép người gọi xử lý trường hợp không tìm thấy, một chuỗi flatMap gộp bốn lần kiểm tra null thành một biểu thức, những mapper không bao giờ nhìn thấy null vì chúng không hề chạy trên nhánh empty. Mọi thứ tệ hại ở nó đều xuất phát từ việc dùng nó ở chỗ khác — một field không serialize được, một parameter có ba trường hợp thay vì hai, một map với hai kiểu rỗng, và cái return null; giữ nguyên bug mà lại thêm wrapper. Hãy học cho kỹ orElse so với orElseGet, vì đó là cái vô hình khi review và nó đã gọi vào nguồn dữ liệu default bốn lần thay vì một trong vòng lặp ở trên.
Đến đây là hết Phần 3 của khoá này. Phần 4 chuyển sang multithreading, bắt đầu từ đầu: một thread thực sự là gì, JVM cấp cho mỗi thread những gì, và tạo một thread bằng Thread cũng như bằng Runnable ra sao.