Java — Từ Zero đến Senior/Optional — xử lý null không vỡ
~20 phútStream API & LambdaMiễn phí

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 orElse vs orElseGet — 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ườngOptional
Phong bì có thưOptional.of(value)
Phong bì rỗngOptional.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!
💡 Intent chính của Optional

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ọi get() → verbose, dùng orElseThrow().

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): expr luô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
⚠️ Lỗi kinh điển

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 orElseorElseGet 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:

  1. 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.

  2. Serialization: Optional không implement Serializable (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).

  3. 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ải if (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

📚 Deep Dive Oracle

Spec / reference chính thức:

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ếu x null; ofNullable(x) safe, null → empty. Dùng ofNullable cho 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).
  • orElse vs orElseGet: orElse luôn evaluate argument; orElseGet chỉ gọi supplier khi empty. Default đắt (DB, I/O, allocation) → bắt buộc orElseGet.
  • 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

Tự kiểm tra
Q1
Khác biệt giữa orElse(default)orElseGet(() -> default)?
  • orElse(expr): eagerexpr evaluate mọi lần, kể cả khi Optional có value. expr là 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) → orElse OK.
  • 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.

Q2
Vì sao không nên dùng Optional làm field của class?

3 lý do cốt lõi:

  1. 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.
  2. 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.
  3. Không giải quyết null: field Optional<String> email vẫn có thể là null (biến Optional ấy). Bạn vẫn phải if (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)

Phân tích từng bước:

  • Optional.of("hello")Optional["hello"].
  • .map(String::length)Optional[5].
  • .filter(n -> n > 10)5 > 10 false → Optional.empty().
  • .orElse(-1) → empty → trả -1.
-1

Chain 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.

Q4
Khi nào dùng 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<...>>>.

Q5
Vì sao 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.

Q6
Optional đượ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?