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?