Chuyển tới nội dung chính

Optional và xử lý ngoại lệ

Bài trước bàn về cú pháp gọn hơn. Bài này bàn về an toàn dữ liệu và xử lý lỗi: dùng Optional để tránh ngoại lệ con trỏ rỗng NullPointerException, khai thác enum ở mức nâng cao, hiểu phân cấp ngoại lệ, dùng try-with-resources để đóng tài nguyên tự động, và định nghĩa ngoại lệ riêng (custom exception) cho miền nghiệp vụ.

Lỗi con trỏ rỗng và việc xử lý lỗi cẩu thả là hai nguồn hỏng hóc lớn nhất trong phần mềm thực tế. Một hàm trả về null khi không tìm thấy dữ liệu trông vô hại, nhưng nó đẩy trách nhiệm kiểm tra sang người gọi, và chỉ cần một chỗ quên kiểm tra là chương trình sập giữa chừng. Tương tự, bắt ngoại lệ rồi bỏ qua sẽ giấu đi triệu chứng của một sự cố thật, khiến việc gỡ lỗi về sau tốn gấp nhiều lần. Java hiện đại đưa ra hai công cụ để chặn hai loại lỗi này ngay từ thiết kế: Optional biến trường hợp rỗng thành thứ người gọi buộc phải nhìn thấy, còn cơ chế ngoại lệ có phân tầng giúp phân biệt lỗi nào phải xử lý bắt buộc, lỗi nào phản ánh sai sót lập trình.

Thử ngay: xem orElse và orElseGet chạy từng bước

Optional cho hai cách khai báo giá trị dự phòng trông na ná nhau là orElseorElseGet, nhưng chạy ra hai kết quả khác nhau. Bấm chạy từng dòng đoạn dưới, trên một Optional đang CÓ giá trị, để thấy khác biệt lộ ra ở đâu: một bên vẫn gọi hàm tạo mặc định và in thêm một dòng, bên kia thì không. Chưa cần hiểu hai phương thức này làm gì, cứ xem trước đã, mục "orElse hay orElseGet" phía dưới sẽ giải thích vì sao.

orElse tính sớm, orElseGet tính lười java
1import java.util.Optional;
2
3public class Demo {
4 static String taoMacDinh() {
5 System.out.println("goi taoMacDinh");
6 return "khach";
7 }
8
9 public static void main(String[] args) {
10 Optional<String> co = Optional.of("Hao");
11 String a = co.orElse(taoMacDinh());
12 System.out.println("a = " + a);
13 String b = co.orElseGet(() -> taoMacDinh());
14 System.out.println("b = " + b);
15 }
16}
Ngăn xếp stack
main()
co= Optional["Hao"]
Bộ nhớ động heap
(trống)
Tạo một Optional CÓ giá trị: co chứa chuỗi "Hao".
1/7

Optional để tránh lỗi con trỏ rỗng

Trả về null để báo không có giá trị là nguồn gốc của lỗi phổ biến nhất trong Java: ngoại lệ con trỏ rỗng NullPointerException, xảy ra khi gọi phương thức trên một tham chiếu null. Từ Java 8, Optional<T> là một hộp chứa, hoặc có đúng một giá trị, hoặc rỗng, buộc người gọi phải xử lý trường hợp rỗng một cách tường minh.

public Optional<SinhVien> timTheoMa(String ma) {
SinhVien sv = kho.get(ma); // may be null
return Optional.ofNullable(sv); // wrap into an Optional
}

// the caller must deal with the empty case explicitly
String ten = timTheoMa("SV01")
.map(SinhVien::getTen) // transform if present
.orElse("khong tim thay"); // fallback if empty
Luồng xử lý một Optional
Bọc giá trị
Dùng Optional.ofNullable để gói kết quả có thể null vào một hộp chứa.
Biến đổi nếu có
Gọi map để chuyển giá trị khi có mặt, không cần kiểm tra null thủ công.
Định giá trị dự phòng
Dùng orElse hoặc orElseThrow để xử lý trường hợp rỗng một cách tường minh.
Nhận kết quả an toàn
Người gọi luôn có giá trị hợp lệ, tránh được ngoại lệ con trỏ rỗng.
ghi chú

Không dùng Optional làm kiểu của trường hay tham số, và tránh gọi get() mà chưa kiểm tra isPresent(): gọi get() trên một Optional rỗng lại ném ngoại lệ, đúng thứ ta định tránh. Hãy ưu tiên map, orElse, orElseThrow, ifPresent.

Optional dùng ở đâu là đúng chỗ

Optional sinh ra cho một chỗ duy nhất: kiểu trả về của phương thức có thể không tìm thấy gì. Ở đó nó nói với người gọi điều mà null không nói được, rằng "rỗng là một kết quả hợp lệ, hãy xử lý đi".

Đừng dùng Optional làm kiểu tham số: người gọi phải bọc giá trị lại chỉ để truyền vào, mà bản thân tham số ấy vẫn có thể là null, thành ra kiểm tra hai lớp; cách đúng là nạp chồng phương thức. Đừng dùng nó làm kiểu trường: mỗi đối tượng cõng thêm một lớp bọc, mà Optional lại không tuần tự hoá được. Và đừng trả về Optional của một tập hợp, vì danh sách rỗng đã tự nói lên là không có gì.

import java.util.List;
import java.util.Optional;

public class ThietKeOptional {
// good: Optional as a return type, the caller cannot ignore the empty case
public Optional<String> timGhiChu(String ma) {
return Optional.empty();
}

// bad: Optional as a parameter -> use overloading instead
// public void luu(String ten, Optional<String> ghiChu) { }
public void luu(String ten) { luu(ten, ""); }
public void luu(String ten, String ghiChu) { System.out.println(ten + " / " + ghiChu); }

// good: an empty list already means "nothing found"
public List<String> timTatCa(String ma) {
return List.of();
}
}

Có một phản mẫu tinh vi hơn cả việc gọi get() bừa: cặp if (opt.isPresent()) return opt.get();. Nó biên dịch trơn tru và không bao giờ ném lỗi, nhưng đã dịch ngược Optional về đúng lối kiểm tra null thủ công mà ta muốn bỏ, chỉ khác là dài dòng hơn. Ý định thật của Optional là để cả nhánh có giá trị và nhánh rỗng cùng chảy qua một biểu thức, nên gần như mọi lần bạn định viết isPresent rồi get, đã có sẵn một phương thức gọn hơn: map cho biến đổi, filter cho điều kiện, ifPresent cho tác dụng phụ, orElseGet cho giá trị dự phòng.

Các phương thức Optional thường dùng

Optional không chỉ có ofNullable, maporElse. Nắm bộ phương thức đầy đủ giúp chọn đúng công cụ cho từng tình huống:

  • Optional.of(x): bọc một giá trị chắc chắn khác null; nếu xnull thì ném ngay NullPointerException.
  • Optional.ofNullable(x): bọc giá trị có thể null, cho ra hộp rỗng khi xnull.
  • Optional.empty(): tạo một hộp rỗng tường minh.
  • map(f): biến đổi giá trị khi có mặt, trả về Optional mới; flatMap dùng khi f đã trả về sẵn một Optional.
  • filter(p): giữ giá trị nếu thoả điều kiện, ngược lại thành rỗng.
  • orElse(v): trả giá trị dự phòng v khi rỗng; orElseGet(supplier) chỉ tính giá trị dự phòng khi thật sự cần.
  • orElseThrow(...): ném ngoại lệ do ta chọn khi rỗng, thay cho việc trả giá trị.
  • ifPresent(consumer)ifPresentOrElse(...): chạy hành động khi có, hoặc rẽ nhánh có và không.
Optional<SinhVien> tim = timTheoMa("SV01");

// chuoi bien doi va loc, ket thuc bang gia tri du phong
String hoTen = tim
.filter(sv -> sv.getDiem() >= 5.0) // chi giu sinh vien dat
.map(SinhVien::getTen) // lay ten neu con gia tri
.orElse("khong hop le"); // du phong khi rong

// khac biet orElse va orElseGet: orElseGet chi goi khi rong
String ten = tim.map(SinhVien::getTen)
.orElseGet(() -> taoTenMacDinh()); // khong goi taoTenMacDinh neu co ten

// nem ngoai le rieng khi rong
SinhVien sv = tim.orElseThrow(
() -> new IllegalStateException("khong thay sinh vien"));
of hay ofNullable

Dùng of khi bạn chắc chắn giá trị khác null và muốn phát hiện sớm nếu giả định đó sai. Dùng ofNullable khi giá trị hợp lệ được phép là null. Nhầm hai cái này là lỗi hay gặp: gọi of trên một giá trị null sẽ ném NullPointerException ngay tại chỗ bọc.

map hay flatMap

Chọn giữa mapflatMap chỉ phụ thuộc một câu hỏi: hàm biến đổi trả về giá trị trần hay đã trả về sẵn một Optional? Nếu nó trả về giá trị trần thì dùng map. Nếu nó đã trả về Optional mà ta vẫn dùng map, kết quả sẽ là một Optional lồng trong Optional, rất khó dùng. flatMap làm phẳng đúng một tầng ấy.

import java.util.Optional;

public class TimThanhPho {
record DiaChi(String thanhPho) { }

record SinhVien(String ten, DiaChi diaChi) {
Optional<DiaChi> timDiaChi() { // this accessor returns an Optional
return Optional.ofNullable(diaChi);
}
}

public static String thanhPhoCua(SinhVien sv) {
return Optional.ofNullable(sv)
.flatMap(SinhVien::timDiaChi) // flatMap: the function returns Optional
.map(DiaChi::thanhPho) // map: the function returns a plain String
.filter(tp -> !tp.isBlank()) // drop blank city names
.orElse("chua ro"); // one explicit fallback at the end
}
}

Cả chuỗi trên không có lấy một phép so sánh với null mà vẫn an toàn ở mọi mắt xích: sinh viên rỗng, địa chỉ rỗng, hay tên thành phố trắng đều chảy về nhánh rỗng và kết thúc ở orElse.

orElse hay orElseGet

Đây là lời giải thích cho khối chạy thử ở đầu bài. Hai phương thức cho cùng kết quả nhưng khác nhau ở thời điểm tính giá trị dự phòng. orElse(v) nhận một giá trị, mà đối số trong Java luôn được tính trước khi lời gọi diễn ra, nên v được tính kể cả khi Optional có giá trị. orElseGet(supplier) nhận một hàm, và hàm ấy chỉ chạy khi hộp thật sự rỗng.

import java.util.Optional;

public class DuPhong {
static String taoMacDinh() {
System.out.println("dang tao gia tri du phong"); // a visible side effect
return "khach";
}

public static void chay() {
Optional<String> co = Optional.of("Hao");

String a = co.orElse(taoMacDinh());
// taoMacDinh() already ran before orElse was even called
System.out.println(a); // prints: Hao

String b = co.orElseGet(DuPhong::taoMacDinh);
// the supplier is never called, because the Optional has a value
System.out.println(b); // prints: Hao
}
}

Chạy đoạn trên sẽ thấy dòng "dang tao gia tri du phong" in đúng một lần, do orElse gây ra. Với hằng số rẻ như orElse("") thì khác biệt không đáng kể. Nhưng nếu giá trị dự phòng là một truy vấn cơ sở dữ liệu, một lời gọi mạng, hay có tác dụng phụ, hãy luôn dùng orElseGet.

Nguyên nhân gốc của khác biệt này nằm ở luật tính đối số của Java: mọi biểu thức truyền vào một phương thức đều được tính xong trước khi phương thức bắt đầu chạy. Vì orElse nhận một giá trị đã tính, còn orElseGet nhận một hàm chưa gọi, nên chỉ orElseGet mới hoãn được công việc tới đúng lúc rỗng. Cùng lý do đó, orElseThrow có phiên bản nhận một hàm dựng ngoại lệ, để đối tượng ngoại lệ chỉ được tạo khi thật sự cần ném, không phí công dựng vết ngăn xếp cho nhánh không bao giờ xảy ra. Trong một vòng lặp nóng chạy hàng vạn lần, chọn nhầm orElse cho một giá trị dự phòng đắt là một rò rỉ hiệu năng lặng lẽ, không lỗi, không cảnh báo, chỉ chậm.

Enum nâng cao

Kiểu liệt kê (enum) không chỉ là một danh sách hằng. Mỗi hằng enum là một đối tượng, nên enum có thể có trường, hàm dựng và phương thức; thậm chí mỗi hằng có thể ghi đè phương thức riêng. Nhờ vậy enum trở thành nơi gom cả dữ liệu và hành vi gắn với từng trường hợp cố định, thay cho chuỗi hằng số rời rạc.

public enum HocLuc {
GIOI(8.0), KHA(6.5), TRUNG_BINH(5.0), YEU(0.0); // each constant carries data

private final double nguong; // field per constant

HocLuc(double nguong) { // enum constructor
this.nguong = nguong;
}

public static HocLuc tu(double diem) { // behavior on the type
if (diem >= GIOI.nguong) return GIOI;
if (diem >= KHA.nguong) return KHA;
if (diem >= TRUNG_BINH.nguong) return TRUNG_BINH;
return YEU;
}
}

Phân cấp ngoại lệ

Mọi ngoại lệ trong Java đều là đối tượng kế thừa từ Throwable. Cây phân cấp chia làm hai nhánh quan trọng. Error là sự cố nghiêm trọng của máy ảo, chương trình thường không bắt. Exception là phần ta xử lý, lại chia tiếp thành ngoại lệ kiểm tra (checked), bắt buộc khai báo hoặc bắt, ví dụ IOException; và ngoại lệ thời gian chạy (unchecked), kế thừa RuntimeException, ví dụ NullPointerException hay IllegalArgumentException.

Throwable
├── Error (khong bat)
└── Exception
├── IOException (checked)
└── RuntimeException (unchecked)
├── NullPointerException
└── IllegalArgumentException

Ranh giới checked và unchecked không phải chuyện cú pháp, nó là chuyện ai chịu trách nhiệm. Ngoại lệ kiểm tra dành cho sự cố ngoài tầm kiểm soát mà người gọi có thể cứu vãn: tệp không tồn tại, mạng đứt, cơ sở dữ liệu từ chối kết nối. Trình biên dịch bắt khai báo throws hoặc bắt lại, chính là để người gọi không thể vờ như nó không xảy ra. Ngoại lệ thời gian chạy thì ngược lại, nó tố cáo một sai sót lập trình: chỉ số vượt mảng, tham số vô lý, gọi phương thức trên tham chiếu rỗng. Cách chữa là sửa mã, không phải bắt lại. Khi tự thiết kế: có hành động phục hồi rõ ràng thì cho checked, không ai phục hồi nổi thì cho unchecked, đừng bắt người gọi viết try cho có.

Cũng vì ranh giới này là chuyện trách nhiệm chứ không phải cú pháp, ngoại lệ kiểm tra hay bị lạm dụng. Ép người gọi bắt một lỗi mà họ không làm gì được ngoài việc gói lại và ném tiếp chỉ đẻ ra những khối catch chép đi chép lại, và tệ hơn là dụ người ta viết catch rỗng cho yên trình biên dịch. Một cách chẩn đoán đơn giản trước khi cho một ngoại lệ thành checked: hãy tự hỏi người gọi có hành động phục hồi cụ thể nào không, ví dụ thử lại, đổi đường dẫn, hay báo người dùng nhập lại. Nếu câu trả lời là không, đó gần như chắc chắn là một sai sót lập trình và nên để unchecked.

try-catch-finally

Trước khi có try-with-resources, cấu trúc nền tảng để xử lý lỗi là try-catch-finally. Khối try bao đoạn mã có thể ném ngoại lệ; mỗi khối catch bắt một loại ngoại lệ và xử lý; khối finally luôn chạy dù có lỗi hay không, kể cả khi khối try đã return, nên là nơi truyền thống để dọn dẹp tài nguyên.

BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("diem.txt"));
System.out.println(br.readLine());
} catch (FileNotFoundException e) {
System.err.println("khong thay tep"); // bat loai cu the truoc
} catch (IOException e) {
System.err.println("loi doc: " + e.getMessage()); // loai chung sau
} finally {
if (br != null) {
try { br.close(); } catch (IOException e) { /* danh dau da thu dong */ }
}
}

Thứ tự các khối catch phải đi từ loại cụ thể tới loại chung: nếu đặt IOException trước FileNotFoundException, nhánh dưới không bao giờ chạy tới và trình biên dịch báo lỗi. Có thể gộp nhiều loại không cùng nhánh kế thừa vào một catch bằng dấu gạch đứng, ví dụ catch (IOException | SQLException e). Đoạn mã trên cho thấy dọn tài nguyên thủ công trong finally rườm rà thế nào, và đó chính là lý do try-with-resources ra đời.

try-with-resources

Tài nguyên như tệp hay kết nối mạng phải được đóng dù có lỗi hay không. Viết tay khối finally để đóng dễ sót và dài dòng. Từ Java 7, try-with-resources tự đóng mọi tài nguyên khai báo trong dấu ngoặc của try, miễn là chúng cài giao tiếp AutoCloseable.

// the resource declared in parentheses is closed automatically, even on error
try (var br = new BufferedReader(new FileReader("diem.txt"))) {
String dong;
while ((dong = br.readLine()) != null) {
System.out.println(dong);
}
} catch (IOException e) {
System.err.println("loi doc tep: " + e.getMessage());
}

Khai báo được nhiều tài nguyên trong cùng một cặp ngoặc, ngăn cách bằng dấu chấm phẩy. Chúng được đóng theo thứ tự ngược với thứ tự khai báo, đúng thứ ta cần: tài nguyên khai báo sau thường phụ thuộc vào tài nguyên khai báo trước, nên phải đóng trước.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class ChepTep {
public static void chep(String nguon, String dich) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(nguon));
BufferedWriter bw = new BufferedWriter(new FileWriter(dich))) {
String dong;
while ((dong = br.readLine()) != null) {
bw.write(dong);
bw.newLine();
}
} // bw is closed first, then br, even if an exception is thrown
}
}

Còn một chi tiết tinh tế: nếu thân try ném một lỗi mà lời close cũng ném một lỗi nữa, tự viết finally sẽ mất một trong hai. try-with-resources giữ cả hai: lỗi của thân try là lỗi chính, lỗi lúc đóng được gắn kèm dưới dạng ngoại lệ bị chèn (suppressed exception), lấy ra qua e.getSuppressed().

Ngoại lệ riêng (custom exception)

Khi miền nghiệp vụ có lỗi đặc thù, ta nên định nghĩa lớp ngoại lệ riêng để thông điệp rõ nghĩa và để người gọi bắt đúng loại. Một ngoại lệ riêng chỉ là một lớp kế thừa Exception nếu muốn checked, hoặc RuntimeException nếu muốn unchecked.

public class KhongDuDiemException extends Exception { // checked custom exception
public KhongDuDiemException(String thongDiep) {
super(thongDiep); // pass the message to the base
}
}

void ghiDanh(SinhVien sv) throws KhongDuDiemException {
if (sv.getDiem() < 5.0) {
throw new KhongDuDiemException("diem duoi nguong: " + sv.getDiem());
}
}

Khi ngoại lệ riêng dùng để gói lại một lỗi ở tầng dưới, hàm dựng phải nhận thêm nguyên nhân (cause) và chuyển tiếp cho lớp cha. Bỏ nguyên nhân là vứt mất vết ngăn xếp gốc: người gỡ lỗi chỉ còn thấy thông điệp chung chung của ta thay vì dòng mã thật sự hỏng.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class LoiCauHinh extends RuntimeException { // unchecked: caller cannot recover
public LoiCauHinh(String thongDiep, Throwable nguyenNhan) {
super(thongDiep, nguyenNhan); // keep the original stack trace
}

public static String doc(Path p) {
try {
return Files.readString(p);
} catch (IOException e) {
// wrap into a domain exception, but never drop the cause
throw new LoiCauHinh("khong doc duoc cau hinh: " + p, e);
}
// catch (IOException e) { return ""; } // swallowing: the bug becomes invisible
}
}

Dòng bị chú thích cuối cùng minh hoạ việc nuốt ngoại lệ: chương trình vẫn chạy, không ai thấy gì, và một tệp cấu hình hỏng lặng lẽ biến thành hành vi sai ở tận đâu đó về sau. Bắt một ngoại lệ chỉ chính đáng khi ta thật sự xử lý được nó, còn không thì hãy để nó đi tiếp lên trên.

Lỗi thường gặp với Optional và ngoại lệ
  • Gọi get() trên Optional mà chưa chắc có giá trị: nếu rỗng sẽ ném NoSuchElementException, đúng loại lỗi ta định tránh; hãy dùng orElse, orElseThrow.
  • Dùng Optional làm kiểu trường trong lớp hay kiểu tham số: sai mục đích thiết kế, chỉ nên dùng làm kiểu trả về.
  • Nuốt lỗi bằng catch rỗng: che mất triệu chứng, gỡ lỗi về sau rất khó.
  • Đặt catch loại chung trước loại cụ thể: nhánh cụ thể không bao giờ chạy, trình biên dịch báo lỗi.
  • Gói lại ngoại lệ mà bỏ nguyên nhân gốc: mất vết ngăn xếp; hãy truyền nguyên nhân qua super(thongDiep, nguyenNhan).
Ghi nhớ

Bắt ngoại lệ ở mức ta xử lý được, không nuốt lỗi bằng khối catch rỗng. Khi gói lại lỗi gốc trong một ngoại lệ mới, hãy truyền nguyên nhân qua hàm dựng super(thongDiep, nguyenNhan) để giữ vết ngăn xếp (stack trace).

Bài tập

Bài 1. Cho Map<String, Integer> kho và một phương thức Integer lay(String khoa) đang trả về null khi không có khoá. Hãy viết lại để trả về Optional<Integer> và minh họa một lời gọi dùng orElse.

Gợi ý

Thân phương thức trở thành return Optional.ofNullable(kho.get(khoa));. Lời gọi ví dụ: int diem = lay("SV01").orElse(0);, trả về 0 khi không có khoá thay vì để null lọt ra ngoài.

Bài 2. Viết enum HuongDi gồm BAC, NAM, DONG, TAY, mỗi hằng mang một độ lệch góc, kèm phương thức doiChieu() trả về hướng đối diện.

Gợi ý

Gắn cho mỗi hằng một trường private final int goc qua hàm dựng, ví dụ BAC(0), DONG(90), NAM(180), TAY(270). Phương thức doiChieu() có thể dùng một switch trả về hằng đối diện, ví dụ BAC -> NAM, hoặc tính theo (goc + 180) % 360 rồi ánh xạ ngược về hằng.

Bài 3. Xếp các lớp sau vào đúng nhánh checked hay unchecked: IOException, FileNotFoundException, NullPointerException, IllegalArgumentException.

Gợi ý

IOExceptionFileNotFoundException là checked, kế thừa Exception nhưng không kế thừa RuntimeException, nên bắt buộc khai báo hoặc bắt. NullPointerExceptionIllegalArgumentException là unchecked, kế thừa RuntimeException, nên không bắt buộc khai báo.

Bài 4. Lập trình. Viết chương trình đọc một tệp văn bản bằng try-with-resources, đếm số dòng. Định nghĩa một ngoại lệ riêng TepRongException ném ra khi tệp không có dòng nào, và xử lý nó ở phương thức main.

Gợi ý

Dùng BufferedReader trong try-with-resources để tự đóng tệp. Đếm dòng bằng một biến đếm trong vòng while. Cho TepRongException extends Exception, sau vòng lặp nếu biến đếm bằng 0 thì throw new TepRongException(...). Ở main, bao lời gọi trong try ... catch (TepRongException e) để in thông báo rõ nghĩa thay vì để chương trình dừng đột ngột.

Bài 5. Cho Optional<SinhVien> tim(String ma). Viết phương thức String emailCua(String ma) trả về email viết thường của sinh viên, dùng flatMapgetEmail() trả về Optional<String>, bỏ qua email trắng, và dự phòng bằng một chuỗi sinh ra từ hàm emailMacDinh() tốn kém. Giải thích vì sao phải dùng orElseGet chứ không phải orElse.

Lời giải

getEmail() đã trả về Optional<String> nên phải dùng flatMap, nếu dùng map sẽ nhận về Optional lồng nhau. filter loại email trắng. Cuối chuỗi phải là orElseGet, vì đối số của orElse luôn được tính trước lời gọi, nghĩa là emailMacDinh() sẽ chạy ngay cả khi sinh viên đã có email hợp lệ, gây lãng phí hoặc tác dụng phụ ngoài ý muốn.

import java.util.Optional;

public class DanhBa {
record SinhVien(String ten, String email) {
Optional<String> getEmail() { return Optional.ofNullable(email); }
}

Optional<SinhVien> tim(String ma) {
return Optional.empty(); // stub for the exercise
}

static String emailMacDinh() { // pretend this one is expensive
return "no-reply@dau.edu.vn";
}

public String emailCua(String ma) {
return tim(ma)
.flatMap(SinhVien::getEmail) // flatMap: getEmail returns an Optional
.filter(e -> !e.isBlank()) // drop blank addresses
.map(String::toLowerCase) // map: toLowerCase returns a plain String
.orElseGet(DanhBa::emailMacDinh); // computed only when empty
}
}

Câu hỏi tự kiểm

Kiểm tra nhanh: Optional và ngoại lệ0/4 đúngchưa trả lời
  1. 1Khác biệt cốt lõi giữa orElse(v) và orElseGet(supplier) là gì?
  2. 2Optional nên dùng ở vị trí nào?
  3. 3Khi hàm biến đổi đã trả về sẵn một Optional, nên dùng phương thức nào để tránh Optional lồng trong Optional?
  4. 4IOException thuộc loại ngoại lệ nào?

Tóm tắt

  • Optional chỉ nên là kiểu trả về của phương thức có thể không tìm thấy gì; đừng dùng làm trường, làm tham số, hay bọc quanh một tập hợp.
  • Chuỗi map, flatMap, filter xử lý trường hợp rỗng mà không cần một phép so sánh null nào; dùng flatMap khi hàm biến đổi đã trả về sẵn một Optional.
  • orElse tính giá trị dự phòng ngay cả khi không cần đến nó; giá trị dự phòng nào đắt hay có tác dụng phụ thì phải dùng orElseGet.
  • Checked là lỗi người gọi có thể phục hồi nên trình biên dịch bắt xử lý; unchecked tố cáo sai sót lập trình, cách chữa là sửa mã chứ không phải bắt lại.
  • try-with-resources đóng nhiều tài nguyên theo thứ tự ngược và giữ lại lỗi lúc đóng dưới dạng ngoại lệ bị chèn.
  • Ngoại lệ riêng phải mang theo nguyên nhân gốc; catch rỗng là cách nhanh nhất để biến một lỗi thấy được thành một lỗi vô hình.