Optional — xử lý null không vỡ
Optional<T> — container có hoặc không có value. API map/filter/flatMap/orElse/orElseThrow. Khi nào dùng và khi nào KHÔNG — vì Optional không phải replacement cho null ở mọi chỗ.
Năm 1965, Tony Hoare thiết kế ngôn ngữ ALGOL W và thêm con trỏ null — "vì nó quá dễ implement". 44 năm sau, ông nói tại hội nghị QCon 2009:
"I call it my billion-dollar mistake. It has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years."
NullPointerException là exception phổ biến nhất trong Java — mọi dev đều đã gặp. user.getAddress().getCity().toUpperCase() — một trong ba method trả null → NPE. Không compile error, không warning. Crash runtime.
Java 8 thêm Optional<T> — container "có hoặc không có value", buộc caller handle trường hợp empty rõ ràng. Nghe như giải pháp hoàn hảo — nhưng thực tế nhiều dev dùng sai: thay null bằng Optional.ofNullable(x).orElse(null) — code thêm dài mà không safer hơn.
Bài này giải thích:
- Optional là gì, API cốt lõi (
map,filter,flatMap,orElse,orElseThrow). - Khi nào NÊN dùng Optional (return type).
- Khi nào KHÔNG dùng (field, parameter, collection) — và tại sao.
- Khác biệt
orElsevsorElseGet— một lỗi performance phổ biến.
1. Analogy — Phong bì có hoặc không có thư
Bạn kiểm tra hộp thư. Trong đó có 1 phong bì, nhưng bạn chưa biết bên trong có thư thật hay phong bì rỗng. Có 2 cách xử lý:
- Xé luôn ra đọc: nếu rỗng → không có gì đọc → lỗi. (Đây là
get()mà không check.) - Kiểm tra trước: "có thư không?" — nếu có thì đọc, không thì xử lý khác. (Đây là
isPresent()+get().) - Functional: "nếu có thư thì làm X, nếu không làm Y" — 1 câu. (Đây là
map,ifPresent,orElse.)
Optional<T> ép caller chọn một trong ba — không cho xé mà không check. Compiler không bắt bạn check, nhưng API structure guide bạn đến đúng pattern.
Ngược lại, với T nullable thường, không có gì bảo bạn null hay không. Javadoc có thể viết, có thể không. Code caller "quên" check → NPE runtime.
| Đời thường | Optional |
|---|---|
| Phong bì có thư | Optional.of(value) |
| Phong bì rỗng | Optional.empty() |
| Hộp thư (không biết có phong bì hay không) | Optional.ofNullable(nullableValue) |
| Kiểm tra có thư | isPresent(), isEmpty() |
| Đọc nếu có | ifPresent(consumer), map(fn), orElse(default) |
| Xé bất kể (nguy hiểm) | get() — avoid! |
Optional không phải "bỏ null ở mọi chỗ". Nó là signal rõ ràng: "kết quả này có thể vắng mặt, caller chuẩn bị xử lý đi". Khi bạn thấy Optional<User> findById(int) — bạn biết ngay có thể empty, không cần đọc doc.
2. Tạo Optional — 3 factory
import java.util.Optional;
// 1. Biet chac khong null
Optional<String> a = Optional.of("hello");
// 2. Co the null
Optional<String> b = Optional.ofNullable(maybeNullVar);
// 3. Rong tuong minh
Optional<String> c = Optional.empty();
Khác biệt of vs ofNullable
Optional.of(x): throw NullPointerException nếu x null.
Optional.ofNullable(x): nếu x null → trả empty; else → present.
String name = user.getName(); // Co the null
Optional<String> opt = Optional.of(name); // NPE neu name null!
Optional<String> opt2 = Optional.ofNullable(name); // OK - null thanh empty
Quy tắc: dùng of chỉ khi bạn chắc chắn value không null (vd constant, literal). Với input không chắc, luôn dùng ofNullable.
Pattern sử dụng trong method
public Optional<User> findUser(int id) {
User u = db.findById(id); // co the null
return Optional.ofNullable(u);
}
public Optional<String> getEnv(String key) {
return Optional.ofNullable(System.getenv(key));
}
Caller nhìn signature thấy Optional<T> → biết ngay "có thể empty, tôi phải handle".
3. API cốt lõi — functional thay vì null check
map — transform value bên trong
Optional<String> name = Optional.of("alice");
Optional<Integer> len = name.map(String::length);
// Optional[5]
Optional<String> empty = Optional.empty();
Optional<Integer> e = empty.map(String::length);
// Optional.empty - map khong goi function
Semantic: map(fn) áp function nếu present, giữ empty nếu empty. Không NPE.
Tương đương imperative:
// Imperative
Integer len;
if (name != null) len = name.length();
else len = null;
// Optional
Optional<Integer> len = Optional.ofNullable(name).map(String::length);
Chain nhiều map:
Optional<String> upperName = Optional.ofNullable(user)
.map(User::getName)
.map(String::toUpperCase);
// Neu user null -> empty
// Neu getName() null -> empty
// Neu co name -> upper case
Mỗi map tự handle empty. Không cần if-null giữa mỗi bước.
filter — giữ nếu thoả predicate
Optional<Integer> age = Optional.of(17);
Optional<Integer> adult = age.filter(a -> a >= 18);
// Optional.empty - 17 khong pass predicate
Optional<Integer> age2 = Optional.of(25);
Optional<Integer> adult2 = age2.filter(a -> a >= 18);
// Optional[25]
flatMap — transform trả Optional
Khi function biến đổi trả Optional<R> (không phải R), dùng flatMap để tránh lồng:
record User(int id, Optional<Address> address) { }
record Address(String city) { }
Optional<User> user = findUser(1);
// Sai: Optional<Optional<Address>>
Optional<Optional<Address>> bad = user.map(User::address);
// Dung: Optional<Address>
Optional<Address> good = user.flatMap(User::address);
Cùng quy tắc với Stream.flatMap (bài 9.4): function sinh container → flatMap flatten.
Chain sâu với flatMap:
Optional<String> city = findUser(1)
.flatMap(User::address)
.map(Address::city);
Mỗi bước có thể empty — chain tự short-circuit.
ifPresent — side-effect khi có value
Optional<String> name = Optional.of("Alice");
name.ifPresent(n -> System.out.println("Hi " + n));
// In: Hi Alice
Optional.<String>empty().ifPresent(n -> System.out.println(n));
// Khong in gi
Java 9+ có ifPresentOrElse cho cả 2 nhánh:
name.ifPresentOrElse(
n -> System.out.println("Hi " + n),
() -> System.out.println("Anonymous")
);
Lấy value ra — 4 cách
Optional<String> opt = Optional.ofNullable(findName());
// 1. Default value (eager)
String n1 = opt.orElse("default");
// 2. Default lazy
String n2 = opt.orElseGet(() -> computeDefault());
// 3. Throw neu empty
String n3 = opt.orElseThrow();
// throws NoSuchElementException
String n4 = opt.orElseThrow(() -> new IllegalStateException("Required"));
// 4. get() - khong recommend
String n5 = opt.get();
// throws NoSuchElementException neu empty
Tránh get() — gần như luôn có option tốt hơn:
- Muốn default →
orElse/orElseGet. - Muốn throw custom →
orElseThrow(Supplier). - Đã check
isPresent()xong gọiget()→ verbose, dùngorElseThrow().
4. orElse vs orElseGet — một lỗi performance phổ biến
Hai method trông giống:
String a = opt.orElse("default");
String b = opt.orElseGet(() -> "default");
Cùng cho ra "default" khi opt empty. Nhưng khác cơ bản:
orElse(expr):exprluôn evaluate — kể cả khi opt có value.orElseGet(supplier): supplier chỉ gọi khi opt empty.
Ví dụ khác biệt
Optional<User> cached = cache.get(id);
// BAD: dbFetch() chay du cached co value
User u = cached.orElse(dbFetchUser(id));
// GOOD: dbFetch() chi chay khi cache miss
User u = cached.orElseGet(() -> dbFetchUser(id));
Với orElse, dbFetchUser(id) là argument của method — evaluated trước khi pass. Dù cached có value và orElse không dùng kết quả, method đã chạy xong, trả về. Mỗi lần cache hit vẫn query DB — perf tệ.
Với orElseGet, supplier được evaluate lazy — chỉ gọi .get() khi cần.
Khi nào dùng orElse?
Khi default là giá trị rẻ, không side-effect: literal string, số, constant:
String name = opt.orElse("Anonymous"); // rẻ
int count = opt.orElse(0); // rẻ
List<T> list = opt.orElse(List.of()); // rẻ
Default là expensive computation hay side-effect:
User u = opt.orElseGet(() -> dbFetch()); // I/O
UUID id = opt.orElseGet(UUID::randomUUID); // allocation
String s = opt.orElseGet(this::computeExpensive); // CPU-heavy
Cache hit nhưng DB vẫn query 100%: cached.orElse(dbFetch()). App chạy chậm đều, monitoring thấy DB QPS cao — debug ra mới hiểu. Thay orElse → orElseGet giảm QPS 90%.
5. Optional + Stream (Java 9+)
Java 9 thêm Optional::stream để convert empty/present thành stream 0 hoặc 1 element:
List<Optional<String>> opts = List.of(
Optional.of("a"),
Optional.empty(),
Optional.of("b")
);
List<String> present = opts.stream()
.flatMap(Optional::stream)
.toList();
// [a, b]
flatMap(Optional::stream) bỏ empty, lấy value của present — pattern rất clean cho filter + unwrap.
Bên trong Optional::stream:
- present →
Stream.of(value). - empty →
Stream.empty().
Hữu ích khi collect result từ nhiều call có thể empty:
List<User> found = ids.stream()
.map(this::findUser) // Stream<Optional<User>>
.flatMap(Optional::stream) // Stream<User> - bo empty
.toList();
6. Khi nào dùng Optional — và khi nào KHÔNG
Đây là phần quan trọng nhất. Optional được thiết kế cho 1 use case cụ thể — return type của method có thể không có kết quả. Dùng sai chỗ tạo overhead mà không thêm safety.
✅ Dùng cho return type của method có thể empty
Optional<User> findUser(int id);
Optional<String> getEnv(String key);
Optional<Double> parseDouble(String s);
Optional<Sale> topSale(List<Sale> sales);
Caller nhìn signature hiểu ngay "có thể empty, tôi phải handle". Không cần đọc javadoc. Không có NPE vì compiler biết Optional có API safe.
❌ KHÔNG dùng cho field
// BAD
class User {
private Optional<String> email;
...
}
3 lý do:
-
Overhead: Optional là object thường (16 byte header + 1 reference = 24 byte trên 64-bit JVM). Class có 1 triệu instance → tốn 24MB cho chỉ field wrapping.
-
Serialization:
Optionalkhông implementSerializable(thiết kế cố ý của JDK team — xem javadoc). Jackson/Gson cần config đặc biệt để serialize Optional (chỉ write value, hoặc skip khi empty). -
Không giải quyết gì: field
Optional<String>vẫn có thể null (chính biến Optional ấy). Bạn vẫn phảiif (email != null) email.ifPresent(...). Không safer hơn field nullable.
Thay vào đó: field nullable bình thường + getter trả Optional:
class User {
private String email; // nullable
public Optional<String> getEmail() {
return Optional.ofNullable(email);
}
}
Field lưu trữ bình thường — tối ưu, serializable. Getter expose Optional semantic cho caller.
❌ KHÔNG dùng cho parameter
// BAD
void process(Optional<String> input) { ... }
// Caller:
process(Optional.of("hello")); // Phai wrap - phien
process(Optional.empty()); // Rong - loi thoi
Người gọi phải wrap — verbose. Thay bằng method overload:
void process(String input) { ... }
void process() { process(""); } // default
// Caller:
process("hello");
process();
Hoặc @Nullable annotation + null check:
void process(@Nullable String input) {
String s = input != null ? input : "";
...
}
Rule: Optional không có ý nghĩa với parameter. API nên dùng overload hoặc nullable.
❌ KHÔNG dùng cho collection
// BAD
Optional<List<User>> getUsers();
List có sẵn concept "rỗng" — List.of() là empty list. Wrap thêm Optional tạo 2 case empty:
Optional.empty()→ "không có list".Optional.of(List.of())→ "có list nhưng rỗng".
Caller không phân biệt được 2 case này có ý nghĩa khác nhau không. Semantic không rõ.
Thay vào đó: method trả list empty thay vì null:
List<User> getUsers() {
return results != null ? results : List.of();
}
Quy ước đã rất phổ biến trong Java: method trả collection không bao giờ return null — luôn empty collection. Tiết kiệm Optional wrapper không cần thiết.
❌ KHÔNG dùng Optional như null-safe wrapper cho biến local
// BAD - overkill
Optional<String> x = Optional.of(name);
Optional<Integer> len = x.map(String::length);
Optional thiết kế cho return type boundary. Biến local dùng trực tiếp — bạn biết chắc nó có value vì bạn vừa gán.
7. Pitfall tổng hợp
❌ Nhầm 1: Optional.of(x) khi x có thể null.
User u = dbFind(id);
return Optional.of(u); // NPE neu u null
✅ Optional.ofNullable(u).
❌ Nhầm 2: orElse(expensiveCall()).
return opt.orElse(queryDatabase()); // Query moi lan, du opt co value
✅ orElseGet(() -> queryDatabase()).
❌ Nhầm 3: Optional cho field.
class User { private Optional<String> email; }
✅ Field nullable + getter Optional:
class User {
private String email;
public Optional<String> getEmail() { return Optional.ofNullable(email); }
}
❌ Nhầm 4: Optional<List<X>>.
Optional<List<User>> findUsers();
✅ Trả empty list:
List<User> findUsers(); // return List.of() neu khong co
❌ Nhầm 5: isPresent + get thay vì orElseThrow.
if (opt.isPresent()) {
return opt.get();
} else {
throw new IllegalStateException();
}
✅ Ngắn hơn: return opt.orElseThrow(IllegalStateException::new);
❌ Nhầm 6: Optional làm parameter.
void save(Optional<User> user) { ... }
✅ Method overload:
void save(User user) { ... }
void save() { save(null); }
8. 📚 Deep Dive Oracle
Spec / reference chính thức:
- Optional class — tất cả method, javadoc chi tiết cùng ví dụ.
- OptionalInt, OptionalLong, OptionalDouble — biến thể primitive tránh boxing.
- "Intended Use" - Stuart Marks (JDK engineer) — talk giải thích Optional intent chính xác.
- Tutorial: Using Optional — Oracle article với code ví dụ.
Ghi chú: Stuart Marks (JDK core engineer, thiết kế Optional cùng Brian Goetz) nhấn mạnh Optional ra đời cho Stream API — terminal op như findFirst, min, max trả Optional vì stream có thể rỗng. Không phải "null replacement universal". Dùng Optional ngoài scope này (field, parameter) là ngộ nhận thiết kế — Marks gọi đó là "over-eager adoption".
9. Tóm tắt
Optional<T>= container có hoặc không có value. Signal rõ ràng trong API "kết quả có thể vắng mặt".Optional.of(x)NPE nếuxnull;ofNullable(x)safe, null → empty. DùngofNullablecho input không chắc.- API functional:
map,filter,flatMap,ifPresent,ifPresentOrElse— chain được, no-op khi empty. - Lấy value:
orElse(eager default),orElseGet(lazy default),orElseThrow(throw nếu empty). orElsevsorElseGet:orElseluôn evaluate argument;orElseGetchỉ gọi supplier khi empty. Default đắt (DB, I/O, allocation) → bắt buộcorElseGet.- Tránh
.get()— gần như luôn có option rõ nghĩa hơn. - Dùng Optional cho return type. Không dùng cho field, parameter, collection.
- Field nullable + getter trả Optional = pattern chuẩn cho domain class.
- Method trả collection không bao giờ return null — luôn empty collection. Optional wrap collection là anti-pattern.
Optional::stream+flatMap= cách clean để filter present từ stream.
10. Tự kiểm tra
Q1Khác biệt giữa orElse(default) và orElseGet(() -> default)?▸
orElse(default) và orElseGet(() -> default)?- orElse(expr): eager —
exprevaluate mọi lần, kể cả khi Optional có value.exprlà argument Java thường — evaluate trước khi pass vào method. - orElseGet(supplier): lazy — supplier.get() chỉ gọi khi empty. Không gọi khi Optional có value.
Quy tắc chọn:
- Default rẻ (literal, constant, empty collection) →
orElseOK. - Default đắt (DB query, file read, allocation phức tạp, side-effect) → bắt buộc
orElseGet.
Lỗi kinh điển: cache.get(id).orElse(dbFetch(id)) — DB query mọi lần du cache hit. Đổi sang orElseGet(() -> dbFetch(id)) — DB chỉ query khi cache miss.
Q2Vì sao không nên dùng Optional làm field của class?▸
3 lý do cốt lõi:
- Overhead memory: Optional là object (16 byte header + reference = 24 byte/field trên 64-bit JVM). Class có triệu instance → tốn 24MB cho chỉ wrapper. Không miễn phí, nhất là với domain object hot path.
- Không Serializable: JDK cố ý không implement Serializable cho Optional (để ngăn dùng sai chỗ). Jackson/Gson cần config đặc biệt. RMI, session replication, cache persistent phức tạp thêm.
- Không giải quyết null: field
Optional<String> emailvẫn có thể là null (biến Optional ấy). Bạn vẫn phảiif (email != null) email.ifPresent(...). Không safer.
Pattern đúng: field nullable bình thường + getter trả Optional. Field tối ưu lưu trữ, getter expose Optional semantic cho caller.
Q3Đoạn sau cho kết quả gì? Optional.of("hello").map(String::length).filter(n -> n > 10).orElse(-1)▸
Optional.of("hello").map(String::length).filter(n -> n > 10).orElse(-1)Phân tích từng bước:
Optional.of("hello")→Optional["hello"]..map(String::length)→Optional[5]..filter(n -> n > 10)→5 > 10false →Optional.empty()..orElse(-1)→ empty → trả-1.
-1Chain functional không cần null check từng bước — mỗi op tự handle empty. Imperative tương đương phải có 3 if-null khác nhau.
Q4Khi nào dùng flatMap thay map với Optional?▸
flatMap thay map với Optional?Khi function biến đổi trả về Optional<R>, không phải R.
record User(int id, Optional<Address> address) { }
// map: Optional<Optional<Address>> - nested, sai
Optional<Optional<Address>> bad = user.map(User::address);
// flatMap: Optional<Address> - flattened, dung
Optional<Address> good = user.flatMap(User::address);Quy tắc giống Stream: function sinh ra container → flatMap flatten 1 tầng.
Trong thực tế: chuỗi method chain qua các object có field Optional → thường cần flatMap để tránh Optional<Optional<Optional<...>>>.
Q5Vì sao Optional<List<X>> là anti-pattern?▸
Optional<List<X>> là anti-pattern?List đã có khái niệm "rỗng" — List.of() là empty list. Wrap thêm Optional tạo 2 case empty mà caller khó phân biệt:
Optional.empty()→ "không có list nào" (vd không tìm thấy).Optional.of(List.of())→ "có list nhưng rỗng" (vd tìm thấy 0 kết quả).
Hai case này có ý nghĩa business khác nhau không? Thường không — cả hai đều là "không có kết quả". Caller viết:
opt.orElse(List.of()).forEach(...)cuối cùng xử lý giống nhau. Wrap Optional chỉ thêm boilerplate không cần.
Quy ước đúng: method trả collection không bao giờ return null — luôn empty collection. Caller tự nhiên forEach chạy 0 lần. Không cần Optional.
Rule: null collection là bug API. Optional<Collection> là overkill.
Q6Optional được thiết kế chính cho use case nào? Tại sao dùng ngoài use case đó là sai?▸
Theo Stuart Marks (JDK engineer, thiết kế Optional), Optional ra đời chính cho Stream API: terminal op như findFirst, findAny, min, max, reduce (no-identity) cần trả "có kết quả hoặc không" — stream có thể rỗng.
Dùng ngoài scope này (field, parameter, universal null replacement) tạo overhead mà không thêm safety:
- Field: overhead memory, serialization phức tạp, field vẫn có thể null.
- Parameter: caller phải wrap, verbose, overload dễ hơn.
- Collection return: empty list đã đủ, wrap thừa.
- Biến local: overkill, dùng trực tiếp value đủ.
Đúng use case: return type của method có thể empty. Đây là nơi Optional tăng clarity API và compiler-check null handling.
Khi đọc code, tự hỏi: "caller cần signal gì ở boundary này?" — nếu là "kết quả có thể vắng mặt" thì Optional đúng chỗ.
Bài tiếp theo: Mini-challenge: Sales report với stream pipeline
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên