99 Ngày Java — Ngày 58: Optional
Ngày 56 kết thúc với một lời dặn: findFirst trả Optional, đừng .get() mù quáng. Hôm nay là bài về cái hộp đó — vì sao nó tồn tại, dùng ở đâu (chỉ kiểu trả về), và bộ ba map/flatMap/orElse để đi qua chuỗi có-thể-null mà không một chữ if. Kèm theo là danh sách anti-pattern: isPresent + get chỉ là null check mặc áo mới, orElse gọi DB dù cache đã có, và Optional ở field hay tham số.

Optional là gì — và không phải là gì
Optional<T> là một cái hộp có thể rỗng: có giá trị hoặc không, và kiểu dữ liệu nói điều đó ra thay vì một dòng Javadoc "có thể trả về null" không ai đọc. Đó là toàn bộ lý do nó tồn tại — cho kiểu trả về. Đưa nó vào field, tham số, hay bọc quanh một collection là dùng sai công cụ: người thiết kế JDK cố tình không cho nó Serializable, và JPA, Jackson, equals đều khó chịu với nó.
// OPTIONAL — một cái HỘP có thể rỗng, thay cho việc trả về null.
// Ngày 56: findFirst / findAny trả Optional, vì có thể KHÔNG ai thỏa.
Optional<DonHang> dau = donHang.stream().filter(DonHang::quaHan).findFirst();
// ① BA CÁCH TẠO — và một cái bẫy:
Optional<String> a = Optional.of("An"); // giá trị CHẮC CHẮN khác null
Optional<String> b = Optional.ofNullable(ten); // ten có thể null → hộp rỗng
Optional<String> c = Optional.empty(); // hộp rỗng tường minh
Optional.of(null); // 💥 NullPointerException NGAY ĐÂY
// of() dành cho thứ bạn dám cam đoan
// ② ĐÂY LÀ NULL CHECK MẶC ÁO MỚI — anti-pattern số 1:
if (dau.isPresent()) { // ❌ y hệt if (x != null)
return dau.get().ma(); // get() vẫn có thể 💥 NoSuchElementException
}
return "-";
return dau.map(DonHang::ma).orElse("-"); // ✅ một dòng, không get()
// ③ OPTIONAL LÀ CÔNG CỤ CHO KIỂU TRẢ VỀ — không phải cho mọi chỗ có thể null:
record KhachHang(String ten, Optional<String> email) {} // ❌ field: không Serializable,
// JPA/Jackson khó chịu, equals rối
void gui(Optional<String> email) {} // ❌ tham số: người gọi phải bọc,
// mà vẫn truyền null được
Optional<List<DonHang>> timTatCa() {} // ❌ collection: trả List RỖNG là đủ
Optional<KhachHang> timTheoEmail(String email) {} // ✅ "có thể không thấy" nói bằng KIỂU
// ⭐ Ý ĐỊNH CỦA NGƯỜI THIẾT KẾ JDK: Optional sinh ra để một method trả về "không có kết quả"
// mà không cần null — và người gọi KHÔNG THỂ quên xử lý, vì kiểu trả về nhắc họ.
// Không phải để thay null ở khắp nơi. Nó cũng KHÔNG implements Serializable — cố ý.
// ④ Số nguyên thủy có hộp riêng — tránh boxing:
OptionalInt max = IntStream.of(3, 7, 5).max(); // OptionalInt, OptionalLong, OptionalDouble
int m = max.orElse(0); // getAsInt() thay cho get()ofcho giá trị chắc chắn khác null (null là nổ ngay),ofNullablecho thứ có thể null,emptycho hộp rỗng tường minh.isPresent()rồiget()là null check mặc áo mới — mọi chỗ như vậy đều viết lại được bằngmap/orElse/ifPresenttrong một dòng.Chỉ dùng cho kiểu trả về — không field, không tham số, không
Optional<List>(trả list rỗng); vàOptionalInt/OptionalLongcho số nguyên thủy để khỏi boxing.
map, flatMap, filter — chuỗi không null
Sức mạnh thật của Optional không phải ở chỗ thay null, mà ở chỗ xâu chuỗi: map biến đổi giá trị nếu có, và một bước rỗng thì mọi bước sau tự bỏ qua. Kim tự tháp bốn tầng if (x != null) thành bốn dòng đọc từ trên xuống. Câu hỏi duy nhất cần trả lời ở mỗi bước là hàm này trả giá trị thường hay trả Optional — cái trước là map, cái sau là flatMap, y hệt Stream ở Ngày 55.
// MAP, FLATMAP, FILTER — đi qua chuỗi có-thể-null mà không một chữ if.
// Bài toán quen: lấy thành phố của khách đặt đơn — mỗi mắt xích đều có thể null.
// ❌ Kim tự tháp if:
String thanhPho = null;
if (don != null) {
KhachHang kh = don.khachHang();
if (kh != null) {
DiaChi dc = kh.diaChi();
if (dc != null) thanhPho = dc.thanhPho();
}
}
if (thanhPho == null) thanhPho = "Không rõ";
// ✅ Chuỗi Optional — bước nào rỗng thì các bước sau BỎ QUA, không nổ:
String thanhPho = Optional.ofNullable(don)
.map(DonHang::khachHang) // Optional<KhachHang> — trả null → tự thành rỗng
.map(KhachHang::diaChi) // Optional<DiaChi>
.map(DiaChi::thanhPho) // Optional<String>
.orElse("Không rõ");
// ① MAP hay FLATMAP? Nhìn KIỂU TRẢ VỀ của hàm bạn truyền vào:
Optional<KhachHang> kh = timKhach(id);
kh.map(KhachHang::timEmail); // timEmail trả Optional<String>
// ❌ → Optional<Optional<String>> — hộp lồng hộp
kh.flatMap(KhachHang::timEmail); // ✅ → Optional<String> — flatMap "làm phẳng"
// ⭐ Hàm trả giá trị thường → map. Hàm trả Optional → flatMap. Y hệt Stream ở Ngày 55.
// ② FILTER — điều kiện không thỏa thì hộp thành rỗng:
Optional<DonHang> donLon = Optional.ofNullable(don)
.filter(d -> d.tong() > 1_000_000); // đơn nhỏ → Optional.empty()
// ③ OR (Java 9) — phương án dự phòng cũng là một Optional, và LƯỜI:
Optional<KhachHang> kq = timTrongCache(id)
.or(() -> timTrongDb(id)); // chỉ chạy khi cache rỗng
// ④ STREAM (Java 9) — lọc bỏ các Optional rỗng trong một Stream:
List<String> emails = khachHangs.stream()
.map(KhachHang::timEmail) // Stream<Optional<String>>
.flatMap(Optional::stream) // ⭐ rỗng biến mất, có thì mở hộp
.toList();
// Thay cho .filter(Optional::isPresent).map(Optional::get) — hai bước, và vẫn có get()
// ❗ Đừng "mở hộp rồi đóng lại": Optional.ofNullable(x.orElse(null)) là mùi code —
// thường vì đã lấy giá trị ra quá sớm. Giữ chuỗi cho tới bước cuối rồi mới mở.mapvới hàm trảnullthì tự thành rỗng — đó là lý do chuỗi không bao giờ ném NPE giữa chừng.mapvới hàm trảOptionalcho ra hộp lồng hộpOptional<Optional<T>>— dấu hiệu bạn cầnflatMap.or()là phương án dự phòng lười (chỉ chạy khi rỗng);Optional::streamtrongflatMaplọc bỏ hộp rỗng khỏiStreammà không cầnget.filterbiến điều kiện sai thành hộp rỗng — hợp cho "có khách, và khách đủ điều kiện" trong một chuỗi.
orElse, orElseGet, orElseThrow — và các anti-pattern
Mở hộp có ba cách, và cách phổ biến nhất có bẫy: orElse(x) luôn tính x trước, kể cả khi hộp có giá trị — nên orElse(db.tai(id)) gọi DB ở mọi lần cache hit. orElseGet nhận Supplier và chỉ chạy khi rỗng. orElseThrow dành cho trường hợp "không có là lỗi" — và đó chính là idiom findById(id).orElseThrow(...) bạn sẽ gặp trong mọi service Spring.
// ORELSE, ORELSEGET, ORELSETHROW — mở hộp đúng cách, và các anti-pattern hay gặp.
// ① BẪY LỚN NHẤT: orElse(...) LUÔN TÍNH tham số, kể cả khi hộp CÓ giá trị.
KhachHang kh = cache.tim(id)
.orElse(db.taiKhachHang(id)); // ❌ gọi DB MỖI LẦN — dù cache đã có!
KhachHang kh = cache.tim(id)
.orElseGet(() -> db.taiKhachHang(id)); // ✅ Supplier — chỉ chạy khi rỗng
// ⭐ Quy tắc: orElse cho HẰNG SỐ rẻ tiền ("-", 0, List.of()). Có tính toán → orElseGet.
// ② ORELSETHROW — "không có" là LỖI, không phải giá trị mặc định:
KhachHang kh = repo.findById(id) // Spring Data trả Optional
.orElseThrow(() -> new KhongTimThayException(id)); // ✅ ngoại lệ nghiệp vụ (Ngày 43)
KhachHang kh = repo.findById(id).orElseThrow(); // Java 10+: NoSuchElementException
// ➜ orElseThrow() không tham số đọc rõ hơn get(): "tôi biết có thể nổ, và tôi chấp nhận".
// ③ IFPRESENT / IFPRESENTORELSE (Java 9) — tác dụng phụ, không cần lấy giá trị ra:
timKhach(id).ifPresent(this::guiEmail);
timKhach(id).ifPresentOrElse(this::guiEmail,
() -> log.warn("Không có khách {}", id));
// ④ CÁC ANTI-PATTERN — nhận ra là sửa được ngay:
if (opt.isPresent()) { dung(opt.get()); } // ❌ null check mặc áo mới → ifPresent / map
opt.orElse(null); // ❌ trả null ra ngoài — mất hết ý nghĩa
// (chỉ chấp nhận ở biên với API cũ)
Optional.ofNullable(x).orElse(y); // ❌ chỉ là x != null ? x : y — dùng ba ngôi
opt.isEmpty() ? macDinh : opt.get(); // ❌ cũng vậy — orElse(macDinh)
Optional<List<T>> timTatCa(); // ❌ trả List RỖNG
private Optional<String> email; // ❌ field — không Serializable
Map<Optional<String>, Integer> dem; // ❌ Optional làm khóa — mùi code
Optional.of(x).equals(Optional.of(y)); // ⚠ đúng là so theo giá trị bên trong,
// nhưng có Optional hai bên thì đã sai từ trước
// ⭐ CHỖ ĐÁNG DÙNG NHẤT: lớp service — repo.findById(...).orElseThrow(...) rồi làm việc
// với đối tượng THẬT. Optional sống ngắn: sinh ra ở kiểu trả về, tiêu thụ ngay ở người gọi.
// Nó không phải kiểu dữ liệu để "mang đi khắp nơi".orElsecho hằng số rẻ,orElseGetcho bất cứ thứ gì phải tính,orElseThrowkhi vắng mặt là lỗi — ba câu hỏi, ba method.orElseThrow()không tham số (Java 10) thay choget(): cùng hành vi, nhưng tên nói rõ "tôi chấp nhận ngoại lệ".orElse(null)trả null ra ngoài và xóa sạch lợi ích — chỉ chấp nhận ở biên với API cũ;Optional.ofNullable(x).orElse(y)chỉ là toán tử ba ngôi viết dài.Optionalsống ngắn: sinh ở kiểu trả về, tiêu thụ ngay ở người gọi — service gọiorElseThrowrồi làm việc với đối tượng thật, không truyền hộp đi tiếp.
Bài tập nhỏ
Tìm trong dự án của bạn mọi chỗ
isPresent()đi kèmget()— viết lại từng chỗ bằngmap,ifPresenthoặcorElseThrow.Viết chuỗi
Optional.ofNullable(don).map(...).map(...).orElse(...)cho một cấu trúc ba tầng, rồi thử vớidon = nullvà với tầng giữanull.Đặt
System.out.printlntrong hàm truyền choorElsevà choorElseGet, gọi trên mộtOptionalcó giá trị — đếm số lần in.Cho
List<Optional<String>>, lấy raList<String>chỉ gồm giá trị có mặt bằngflatMap(Optional::stream).Thử
Optional.of(null),Optional.empty().get()vàOptional.empty().orElseThrow()— đọc ba thông điệp lỗi và thấy cái nào rõ nhất.
Kết lại
Bốn ý gói lại hôm nay: Optional là công cụ cho kiểu trả về — không field, không tham số, không bọc collection; map/flatMap/filter xâu chuỗi các bước có-thể-null và một bước rỗng thì mọi bước sau tự bỏ qua, với flatMap khi hàm trả Optional; orElse luôn tính tham số nên việc tốn kém phải nằm trong orElseGet, và orElseThrow là idiom cho "không có là lỗi"; cuối cùng, isPresent + get và orElse(null) là hai anti-pattern xóa sạch lợi ích của cái hộp. Ngày 59 chạm vào cái nút ai cũng từng bấm thử: parallelStream — ForkJoinPool chung, khi nào nên và không nên, và cái bẫy lambda có trạng thái. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


