- Đọc được cây phân cấp
Throwablevà biết mỗi nhánh nghĩa là gì. - Phân biệt dứt điểm checked và unchecked; biết khi nào chọn loại nào.
- Dùng
try/catch/finallyvà multi-catch. - Làm chủ try-with-resources và
AutoCloseable— thay thế hẳnfinallycho tài nguyên. - Phân biệt
throw(ném) vàthrows(khai báo). - Thiết kế custom exception thành cây lỗi cho ứng dụng.
- Giữ nguyên nhân gốc bằng exception chaining.
- Đọc stack trace — kể cả phần
Caused byvàSuppressed. - Tránh 6 anti-pattern kinh điển, đặc biệt là "nuốt ngoại lệ".
- Biết vì sao
finallycóreturnlà bug vàSystem.exit()bỏ quafinally.
1. Cơ chế — điều gì xảy ra khi có ngoại lệ
static void tang3() {
System.out.println(" tầng 3: bắt đầu");
throw new IllegalStateException("hỏng ở đáy");
// mọi dòng sau đây KHÔNG chạy — trình biên dịch còn báo "unreachable statement"
}
static void tang2() {
System.out.println(" tầng 2: gọi tầng 3");
tang3();
System.out.println(" tầng 2: dòng này bị bỏ qua");
}
static void tang1() {
try {
tang2();
} catch (IllegalStateException e) {
System.out.println("tầng 1: bắt được → " + e.getMessage());
}
}
tang1();
// tầng 2: gọi tầng 3
// tầng 3: bắt đầu
// tầng 1: bắt được → hỏng ở đáy
Gỡ ngăn xếp (stack unwinding)
Khi throw chạy, JVM dừng phương thức hiện tại ngay lập tức và bắt đầu đi ngược lên ngăn
xếp lời gọi, tìm khối catch phù hợp. Mỗi khung được gỡ bỏ, và finally của
khung đó (nếu có) được chạy trên đường đi.
Nếu đi hết ngăn xếp mà không ai bắt: luồng kết thúc, JVM in stack trace ra System.err. Nếu
đó là luồng chính, chương trình thoát với mã khác 0. Điều này khác hoàn toàn với mô hình trả về mã lỗi
(kiểu C): mã lỗi có thể bị bỏ qua, ngoại lệ thì không.
2. Cây phân cấp Throwable
| Checked | Unchecked (RuntimeException) | Error | |
|---|---|---|---|
| Trình biên dịch bắt buộc xử lý | ✅ Có | ❌ Không | ❌ Không |
| Nguyên nhân | Điều kiện bên ngoài | Lỗi lập trình | Hệ thống/JVM hỏng |
| Ví dụ | Tệp không tồn tại, mạng đứt, SQL lỗi | Null, chỉ số sai, ép kiểu sai | Hết bộ nhớ, tràn ngăn xếp |
| Nên bắt? | ✅ Có — và xử lý có ý nghĩa | ⚠️ Thường là sửa code, không phải bắt | ❌ Không |
| Người gọi làm gì được? | Có thể thử lại, dùng phương án khác | Không — code sai thì phải sửa | Không |
Error
OutOfMemoryError nghĩa là JVM hết bộ nhớ — khối catch của bạn cũng cần bộ nhớ
để chạy. StackOverflowError nghĩa là ngăn xếp đã đầy. Bắt chúng thường chỉ khiến chương
trình chạy tiếp trong trạng thái hỏng, che giấu vấn đề thật và gây ra lỗi khó hiểu hơn ở nơi khác.
Hệ quả: tránh catch (Throwable t) — nó bắt cả Error. Nếu thật sự cần lưới an
toàn ở biên chương trình, dùng catch (Exception e).
3. try / catch / finally
try {
int[] a = new int[3];
a[5] = 10;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Chỉ số sai: " + e.getMessage());
} finally {
System.out.println("Luôn chạy");
}
// --- Nhiều catch: CỤ THỂ TRƯỚC, TỔNG QUÁT SAU ---
try {
xuLyTep(duongDan);
} catch (FileNotFoundException e) { // con của IOException
System.out.println("Không thấy tệp");
} catch (IOException e) { // cha — phải đứng SAU
System.out.println("Lỗi I/O khác");
}
// Đảo ngược thứ tự → error: exception FileNotFoundException has already been caught
// --- MULTI-CATCH (Java 7+): gộp khi xử lý giống nhau ---
try {
xuLy();
} catch (IOException | SQLException e) { // biến e ngầm là final
log.error("Thao tác thất bại", e);
throw new DichVuException("Không xử lý được", e);
}
// ⚠️ Các kiểu trong multi-catch không được có quan hệ cha-con với nhau
| Khối | Chạy khi nào | Dùng để |
|---|---|---|
try | Luôn | Chứa đúng thao tác có thể ném — càng hẹp càng tốt |
catch | Khi có ngoại lệ khớp kiểu | Xử lý, chuyển đổi, hoặc ghi log |
finally | Luôn — kể cả khi có return | Dọn dẹp (nay thường thay bằng try-with-resources) |
return trong finally nuốt cả ngoại lệstatic int xau() {
try {
return 1;
} finally {
return 2; // ❌ ghi đè giá trị trên
}
}
xau(); // 2
static int ratXau() {
try {
throw new RuntimeException("lỗi nghiêm trọng!");
} finally {
return 0; // ❌❌ NUỐT ngoại lệ — không ai biết có lỗi
}
}
ratXau(); // 0 — RuntimeException biến mất hoàn toàn
// ⚠️ Sửa giá trị trong finally cũng không có tác dụng với kiểu nguyên thủy
static int laLung() {
int x = 1;
try { return x; }
finally { x = 99; } // giá trị trả về đã được "chốt" trước đó
}
laLung(); // 1, không phải 99
Quy tắc cứng: không bao giờ đặt return, break,
continue, hay throw trong finally. Chỉ đặt code dọn dẹp. Mọi
công cụ phân tích tĩnh nghiêm túc (SonarQube, SpotBugs, IntelliJ) đều cảnh báo trường hợp này.
finally không chạytry {
System.exit(0); // JVM thoát ngay lập tức
} finally {
System.out.println("Không bao giờ in ra");
}
Ngoài System.exit(), finally cũng bị bỏ qua nếu JVM bị kill hoặc luồng bị
huỷ ở tầng hệ điều hành. Muốn chạy code lúc thoát bình thường, đăng ký shutdown hook:
Runtime.getRuntime().addShutdownHook(new Thread(...)).
4. try-with-resources — cách quản lý tài nguyên đúng
// ❌ CÁCH CŨ — dài dòng và VẪN CÒN BUG
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("data.txt"));
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close(); // close() cũng ném IOException → phải try lồng!
} catch (IOException e) {
// nếu cả readLine() và close() cùng lỗi, lỗi nào bị mất?
}
}
}
// ✅ TRY-WITH-RESOURCES (Java 7+) — ngắn hơn và ĐÚNG hơn
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
// close() tự động gọi — kể cả khi có ngoại lệ, kể cả khi return
// Nhiều tài nguyên — ĐÓNG THEO THỨ TỰ NGƯỢC (cái tạo sau đóng trước)
try (
Connection conn = DriverManager.getConnection(url, user, pass);
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()
) {
while (rs.next()) { ... }
} // đóng: rs → ps → conn
// Java 9+: dùng biến effectively final đã khai báo trước
BufferedReader reader = new BufferedReader(...);
try (reader) {
...
}
public class DongHo implements AutoCloseable {
private final String ten;
private final long batDau = System.nanoTime();
public DongHo(String ten) { this.ten = ten; }
@Override
public void close() { // AutoCloseable.close() ném Exception;
long ms = (System.nanoTime() - batDau) / 1_000_000;
System.out.printf("%s: %dms%n", ten, ms);
} // thu hẹp còn không ném gì là hợp lệ (chương 5)
}
try (var d = new DongHo("Nạp dữ liệu")) {
napDuLieu();
} // tự in thời gian, kể cả khi napDuLieu() ném lỗi
Suppressed exception — điều try-with-resources làm mà finally không làm được
Tình huống: thân try ném lỗi A, rồi close() cũng ném lỗi B. Với
finally thủ công, lỗi B ghi đè lỗi A — bạn mất nguyên nhân gốc và debug rất khổ.
try-with-resources giữ cả hai: A được ném ra ngoài như bình thường, còn B được đính kèm vào A dưới dạng suppressed exception. Stack trace hiển thị đủ:
Exception in thread "main" java.io.IOException: lỗi khi đọc
at Demo.main(Demo.java:5)
Suppressed: java.io.IOException: lỗi khi đóng
at Demo$TaiNguyen.close(Demo.java:15)
at Demo.main(Demo.java:6)
Lấy bằng code: e.getSuppressed() trả về mảng các ngoại lệ bị chặn.
5. throw vs throws
throw | throws | |
|---|---|---|
| Là gì | Câu lệnh — ném ngoại lệ | Mệnh đề trong chữ ký — khai báo có thể ném |
| Vị trí | Trong thân phương thức | Sau danh sách tham số |
| Số lượng | Một object mỗi lần | Nhiều kiểu, cách nhau bằng dấu phẩy |
| Ví dụ | throw new IOException("x"); | void f() throws IOException, SQLException |
// throws — khai báo: "phương thức này CÓ THỂ ném IOException"
static String docTep(String duongDan) throws IOException {
if (duongDan == null)
throw new IllegalArgumentException("Đường dẫn không được null");
// ↑ unchecked → KHÔNG cần khai báo trong throws
return Files.readString(Path.of(duongDan)); // ném IOException — checked
}
// Người gọi có HAI lựa chọn với checked exception:
// ① BẮT và xử lý
static void cach1() {
try {
String nd = docTep("a.txt");
} catch (IOException e) {
System.out.println("Dùng giá trị mặc định");
}
}
// ② KHAI BÁO tiếp, đẩy trách nhiệm lên người gọi mình
static void cach2() throws IOException {
String nd = docTep("a.txt");
}
// ❌ Không làm gì cả → lỗi biên dịch
// static void cach3() { docTep("a.txt"); }
// error: unreported exception IOException; must be caught or declared to be thrown
// Đây gọi là quy tắc "CATCH OR SPECIFY" — đặc trưng riêng của Java
6. Thiết kế custom exception
// ① Lớp gốc cho toàn module — cho phép bắt gộp mọi lỗi của bạn
public class ThanhToanException extends RuntimeException {
public ThanhToanException(String msg) { super(msg); }
public ThanhToanException(String msg, Throwable nguyenNhan) {
super(msg, nguyenNhan); // ⭐ constructor NÀY rất quan trọng
}
}
// ② Lỗi cụ thể — MANG THEO DỮ LIỆU, không chỉ chuỗi
public class KhongDuSoDuException extends ThanhToanException {
private final long can;
private final long co;
public KhongDuSoDuException(long can, long co) {
super("Cần %,dđ nhưng chỉ có %,dđ (thiếu %,dđ)"
.formatted(can, co, can - co));
this.can = can;
this.co = co;
}
public long getThieu() { return can - co; }
}
public class TheHetHanException extends ThanhToanException {
private final LocalDate ngayHetHan;
public TheHetHanException(LocalDate ngay) {
super("Thẻ hết hạn ngày " + ngay);
this.ngayHetHan = ngay;
}
public LocalDate getNgayHetHan() { return ngayHetHan; }
}
// ③ Người gọi bắt ở nhiều mức chi tiết khác nhau
try {
thanhToan(don);
} catch (KhongDuSoDuException e) {
hienNutNapTien(e.getThieu()); // hành động cụ thể nhờ có dữ liệu
} catch (ThanhToanException e) {
hienThongBaoChung(e.getMessage()); // lưới an toàn cho cả module
}
KhongDuSoDuException mang theo
can và co nên handler có thể hành động (hiện nút "Nạp thêm
150.000đ"). Nếu chỉ có message, handler buộc phải phân tích chuỗi — cách làm dễ vỡ nhất.
| Kế thừa | Khi nào | Ví dụ |
|---|---|---|
RuntimeException ⭐ | Mặc định — người gọi không làm gì được ngoài báo lỗi | Vi phạm quy tắc nghiệp vụ, dữ liệu không hợp lệ |
Exception (checked) | Người gọi thực sự có phương án thay thế | Thử lại, chuyển sang nguồn dữ liệu khác |
Xu hướng hiện đại nghiêng hẳn về unchecked. Spring, Hibernate và hầu hết framework lớn đều bọc
SQLException (checked) thành DataAccessException (unchecked). Lý do ở mục
tiếp theo.
7. Vì sao checked exception gây tranh cãi
// ❌ VẤN ĐỀ 1: "throws lây lan" khắp mọi tầng
void tangDuoi() throws IOException { ... }
void tangGiua() throws IOException { tangDuoi(); }
void tangTren() throws IOException { tangGiua(); }
// Tầng giao diện đâu quan tâm tới IOException — nó chỉ cần biết "thất bại"
// ❌ VẤN ĐỀ 2: dẫn tới NUỐT LỖI vì lười
try {
xuLy();
} catch (IOException e) {
// TODO: xử lý sau
} // 💥 lỗi biến mất — đây là hậu quả TỆ NHẤT của checked exception
// ❌ VẤN ĐỀ 3: không dùng được với lambda (chương 10)
List<String> duongDan = ...;
duongDan.forEach(p -> Files.readString(Path.of(p)));
// ❌ error: unhandled exception type IOException
// Consumer.accept() không khai báo throws → lambda không ném checked được
// ✅ GIẢI PHÁP: bọc thành unchecked ở biên
static String docAnToan(String p) {
try {
return Files.readString(Path.of(p));
} catch (IOException e) {
throw new UncheckedIOException(e); // JDK có sẵn class này!
}
}
duongDan.forEach(p -> System.out.println(docAnToan(p))); // ✅
Bài học lịch sử
Checked exception ra đời năm 1995 với ý tưởng rất hợp lý: ép lập trình viên nghĩ về lỗi. Nhưng
ba mươi năm thực tế cho thấy nó thường phản tác dụng — khi trình biên dịch bắt buộc viết
catch, nhiều người viết một khối rỗng cho xong, và như vậy còn tệ hơn là để ngoại lệ bay
lên.
C# (ra đời 2000, học từ Java) cố tình không có checked exception. Kotlin, Scala, và
mọi ngôn ngữ JVM sau này cũng vậy. Bản thân JDK cũng đi theo hướng đó: Java 8 thêm
UncheckedIOException để bọc IOException.
Lời khuyên thực dụng: với API mới của bạn, dùng unchecked là mặc định. Với checked exception từ thư viện, bọc lại thành unchecked ở biên giữa tầng hạ tầng và tầng nghiệp vụ. Chỉ giữ checked khi người gọi thật sự có phương án thay thế cụ thể.
8. Exception chaining — giữ nguyên nhân gốc
// ❌ SAI — mất hoàn toàn nguyên nhân gốc
try {
conn.executeQuery(sql);
} catch (SQLException e) {
throw new DichVuException("Không lấy được dữ liệu");
// stack trace chỉ trỏ vào dòng này — không biết SQL nào hỏng, vì sao
}
// ✅ ĐÚNG — truyền nguyên nhân vào constructor
try {
conn.executeQuery(sql);
} catch (SQLException e) {
throw new DichVuException("Không lấy được dữ liệu người dùng", e);
}
// Hoặc dùng initCause() nếu exception không có constructor nhận cause
MyException ex = new MyException("lỗi");
ex.initCause(e);
throw ex;
Exception in thread "main" com.vidu.DichVuException: Không lấy được dữ liệu người dùng
at com.vidu.NguoiDungService.tim(NguoiDungService.java:42) ← ② lớp bọc
at com.vidu.Main.main(Main.java:15)
Caused by: java.sql.SQLException: Table 'app.nguoi_dung' doesn't exist
at com.mysql.cj.jdbc.StatementImpl.executeQuery(StatementImpl.java:1200)
at com.vidu.NguoiDungService.tim(NguoiDungService.java:38) ← ① nguyên nhân THẬT
... 1 more
- Đọc từ dưới lên — khối
Caused bycuối cùng chứa nguyên nhân gốc thật sự. - Tìm dòng đầu tiên thuộc code của bạn — bỏ qua các frame của thư viện và JDK. Đó thường là nơi cần sửa.
... 1 morenghĩa là các frame còn lại trùng với stack trace phía trên, đã lược bớt cho gọn.- Trong ví dụ trên: bảng
nguoi_dungkhông tồn tại, phát hiện tạiNguoiDungService.java:38.
9. Sáu anti-pattern kinh điển
// ❌ ① NUỐT NGOẠI LỆ — tội nặng nhất
try { xuLy(); } catch (Exception e) { }
// Lỗi biến mất hoàn toàn. Sau này ai đó sẽ mất cả ngày để tìm ra vì sao
// dữ liệu bị thiếu. Nếu THỰC SỰ muốn bỏ qua, hãy ghi rõ LÝ DO:
try {
tam.delete();
} catch (IOException e) {
log.debug("Không xoá được tệp tạm, hệ điều hành sẽ tự dọn", e);
}
// ❌ ② printStackTrace() trong code production
catch (Exception e) { e.printStackTrace(); }
// Ghi ra System.err, không vào hệ thống log, không có ngữ cảnh,
// không có thời gian, không lọc được theo mức độ.
catch (Exception e) { log.error("Xử lý đơn {} thất bại", donId, e); } // ✅
// ❌ ③ BẮT QUÁ RỘNG
try { xuLy(); } catch (Exception e) { }
// Nuốt luôn cả NullPointerException — che giấu bug thật của bạn
catch (IOException | SQLException e) { } // ✅ nêu rõ loại
// ❌ ④ DÙNG NGOẠI LỆ THAY ĐIỀU KIỆN
try {
while (true) System.out.println(a[i++]);
} catch (ArrayIndexOutOfBoundsException e) { } // dùng lỗi làm điều kiện dừng!
for (int x : a) System.out.println(x); // ✅ rõ ràng và nhanh hơn nhiều
// ❌ ⑤ NÉM Exception TRẦN
public void luu() throws Exception { ... }
// Buộc người gọi bắt Exception — mất mọi thông tin về loại lỗi
public void luu() throws IOException, SQLException { ... } // ✅
// ❌ ⑥ GHI LOG RỒI NÉM LẠI Ở MỌI TẦNG
catch (Exception e) {
log.error("lỗi", e);
throw e; // tầng nào cũng log → cùng một lỗi in ra 5 lần
}
// ✅ Chỉ log ở BIÊN — nơi lỗi dừng lại (handler HTTP, main, vòng lặp worker).
// Tầng giữa: hoặc để nó bay, hoặc bọc thêm ngữ cảnh bằng chaining.
10. Thực hành tốt
// ✅ ① Kiểm tra tham số đầu vào ở ĐẦU phương thức (fail fast)
public void chuyenTien(TaiKhoan tu, TaiKhoan den, long soTien) {
Objects.requireNonNull(tu, "Tài khoản nguồn không được null");
Objects.requireNonNull(den, "Tài khoản đích không được null");
if (soTien <= 0)
throw new IllegalArgumentException("Số tiền phải dương: " + soTien);
if (tu.equals(den))
throw new IllegalArgumentException("Không thể chuyển cho chính mình");
...
}
// ✅ ② Thông điệp lỗi có ĐỦ NGỮ CẢNH để hành động
throw new IllegalArgumentException("Sai"); // ❌ vô dụng
throw new IllegalArgumentException(
"Tuổi phải trong [0, 150], nhận %d cho người dùng %s"
.formatted(tuoi, maNguoiDung)); // ✅
// ✅ ③ Dùng Optional thay vì ném ngoại lệ cho "không tìm thấy"
public Optional<NguoiDung> tim(String id) { ... } // "không có" là bình thường
public NguoiDung bacBuoc(String id) { // "phải có" — không có là lỗi
return tim(id).orElseThrow(() ->
new NguoiDungKhongTonTai(id));
}
// ✅ ④ Khôi phục cờ ngắt khi bắt InterruptedException (chương 12)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ⚠️ BẮT BUỘC — nếu không, tín hiệu dừng bị mất
throw new RuntimeException("Bị ngắt khi đang chờ", e);
}
// ✅ ⑤ Ghi tài liệu bằng @throws trong Javadoc
/**
* Rút tiền khỏi tài khoản.
* @param soTien số tiền, phải dương
* @throws IllegalArgumentException nếu soTien <= 0
* @throws KhongDuSoDuException nếu số dư không đủ
*/
public void rut(long soTien) { ... }
11. Cheat sheet chương 6
| Tình huống | Cách viết |
|---|---|
| Bắt nhiều loại, xử lý giống nhau | catch (IOException | SQLException e) |
| Nhiều loại, xử lý khác nhau | Nhiều catch — cụ thể trước, tổng quát sau |
| Quản lý tài nguyên | try (var r = new X()) { … } — không dùng finally |
| Tự làm tài nguyên đóng được | implements AutoCloseable |
| Kiểm tra null | Objects.requireNonNull(x, "thông điệp") |
| Tham số không hợp lệ | IllegalArgumentException |
| Object sai trạng thái | IllegalStateException |
| Chưa cài đặt | UnsupportedOperationException |
| Bọc checked thành unchecked | throw new UncheckedIOException(e) |
| Giữ nguyên nhân gốc | new MyException("msg", e) |
| Ghi log lỗi | log.error("ngữ cảnh {}", id, e) — không printStackTrace() |
| "Không tìm thấy" là bình thường | Trả Optional<T>, không ném |
Bắt InterruptedException | Luôn Thread.currentThread().interrupt() |
| Custom exception | Mặc định kế thừa RuntimeException; mang theo dữ liệu |
Trong finally | Không bao giờ return/throw |
Bài tập
Cây exception cho thư viện HTTP
Thiết kế hệ thống ngoại lệ cho một HTTP client giả lập:
HttpException (gốc, extends RuntimeException)
├── KetNoiThatBai — mang .host, .port, .nguyenNhan
├── QuaThoiGian — mang .soMili
└── LoiTrangThai — mang .maTrangThai, .noiDung
├── LoiClient — 4xx
└── LoiServer — 5xx
- Viết
static void kiemTraTrangThai(int ma, String body): không làm gì với 2xx/3xx, ném đúng loại con với 4xx/5xx. - Viết một client giả lập ngẫu nhiên ném các lỗi trên.
- Viết hàm
goiCoRetry: thử lại tối đa 3 lần với backoff luỹ thừa, nhưng chỉ retryQuaThoiGian,KetNoiThatBaivàLoiServer—LoiClientphải ném ngay. - Hết số lần vẫn hỏng → ném lỗi cuối cùng, và đính kèm mọi lỗi trước đó dưới dạng suppressed.
Gợi ý
Suppressed: loiCuoi.addSuppressed(loiTruoc) cho từng lần thử thất bại trước đó —
khi in stack trace bạn sẽ thấy đầy đủ lịch sử.
Backoff: Thread.sleep((long) Math.pow(2, lan) * 100), nhớ bắt
InterruptedException và khôi phục cờ ngắt.
Việc phân biệt "lỗi đáng retry" và "lỗi không đáng retry" chính là lý do nên tách cây exception
thành nhiều lớp con, thay vì một exception duy nhất mang mã lỗi.
Chứng minh suppressed exception
Viết class TaiNguyenXau implements AutoCloseable mà cả phương thức làm việc và close() đều ném ngoại lệ. Sau đó so sánh hai cách xử lý:
// Cách A: finally thủ công
TaiNguyenXau r = null;
try {
r = new TaiNguyenXau();
r.lamViec(); // ném "lỗi A"
} finally {
if (r != null) r.close(); // ném "lỗi B"
}
// Cách B: try-with-resources
try (TaiNguyenXau r2 = new TaiNguyenXau()) {
r2.lamViec();
}
Trả lời: với cách A, lỗi nào đến được người gọi? Lỗi kia đi đâu? Với cách B thì sao? In e.getSuppressed() để kiểm chứng.
Gợi ý
Cách A: lỗi B ghi đè lỗi A hoàn toàn — bạn chỉ thấy "lỗi khi đóng" và không bao
giờ biết nguyên nhân thật là gì. Đây là một trong những lý do quan trọng nhất khiến
try-with-resources ra đời, và cũng là lý do không nên quay lại dùng
finally để đóng tài nguyên dù bạn viết nó "đúng".
Sửa sáu anti-pattern
Đoạn code sau mắc đủ sáu anti-pattern đã học. Tìm, sửa, và giải thích hậu quả cụ thể của từng lỗi.
public class XuLyDon {
public void xuLy(String tepDon) throws Exception {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(tepDon));
String dong;
while ((dong = br.readLine()) != null) {
try {
luuVaoDb(phanTich(dong));
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
throw new Exception("Lỗi");
} finally {
try { br.close(); } catch (Exception e) { }
return;
}
}
}
Gợi ý
Danh sách kiểm: (1) throws Exception trần; (2) printStackTrace();
(3) bắt Exception quá rộng — nuốt cả NPE trong phanTich;
(4) new Exception("Lỗi") mất nguyên nhân gốc và không có ngữ cảnh;
(5) catch { } rỗng khi đóng; (6) return trong finally.
Còn một bug nữa: br.close() ném NPE nếu new FileReader đã thất bại.
Bản sửa nên dùng try-with-resources, ghi rõ số dòng bị lỗi, và tiếp tục xử lý các dòng
còn lại thay vì dừng cả tệp.
Trình phân tích cấu hình có báo lỗi tốt
Viết DocCauHinh đọc tệp .properties và trả về object cấu hình đã kiểm tra:
server.port=8080
server.host=localhost
db.url=jdbc:mysql://localhost:3306/app
db.maxConnections=10
app.debug=true
record CauHinh(int port, String host, String dbUrl, int maxConn, boolean debug).- Thiếu khoá bắt buộc →
CauHinhThieuKhoaExceptionmang tên khoá. - Sai định dạng (port không phải số) →
CauHinhSaiDinhDangExceptionmang tên khoá, giá trị thô, kiểu mong đợi. - Giá trị ngoài phạm vi (port không trong 1-65535) →
CauHinhNgoaiPhamViException. - Gom mọi lỗi rồi báo một lượt, thay vì dừng ở lỗi đầu tiên — dùng suppressed exception hoặc một list.
- Tệp không tồn tại → dùng cấu hình mặc định và ghi log cảnh báo, không sập.
Gợi ý
Gom lỗi: thu thập vào List<Exception>, cuối cùng nếu không rỗng thì tạo một
CauHinhKhongHopLeException và addSuppressed() từng lỗi con vào.
Đây là lý do người dùng ghét công cụ báo lỗi từng cái một: sửa một lỗi, chạy lại, gặp lỗi tiếp
theo, lặp mười lần. Báo hết một lượt là trải nghiệm tốt hơn hẳn.
Đo chi phí ngoại lệ
Viết benchmark so sánh ba cách xử lý "phần tử không tồn tại", với 1.000.000 lần lặp:
- Kiểm tra bằng
iftrước khi truy cập. - Bắt
ArrayIndexOutOfBoundsException. - Bắt ngoại lệ nhưng override
fillInStackTrace()để trả vềthis(bỏ qua việc thu thập stack trace).
Lập bảng kết quả và trả lời: phần nào của việc ném ngoại lệ tốn kém nhất?
Gợi ý
Bạn sẽ thấy cách 2 chậm hơn cách 1 hàng trăm lần, còn cách 3 thì chỉ chậm hơn vài lần.
Kết luận: chi phí chính không nằm ở việc ném/bắt, mà ở việc thu thập stack
trace trong constructor của Throwable — nó phải duyệt toàn bộ ngăn xếp.
Đây là lý do kỹ thuật cho nguyên tắc "đừng dùng ngoại lệ cho luồng điều khiển thông thường", và
cũng là lý do vài framework dùng exception singleton không có stack trace cho các trường hợp
xảy ra rất thường xuyên.
Nhớ warm-up JIT: chạy vài lượt và bỏ kết quả lượt đầu.
Quiz
Checked và unchecked exception khác nhau thế nào? Cho ví dụ mỗi loại.
Xem đáp án
Checked — mọi lớp con của Exception ngoại trừ nhánh
RuntimeException. Trình biên dịch bắt buộc bạn phải catch
hoặc khai báo throws (quy tắc "catch or specify"). Chúng biểu thị điều kiện
bên ngoài mà người gọi có thể phản ứng: IOException,
SQLException, ClassNotFoundException, InterruptedException.
Unchecked — RuntimeException và các lớp con. Không bắt buộc xử lý.
Chúng biểu thị lỗi lập trình: NullPointerException,
IllegalArgumentException, ArrayIndexOutOfBoundsException,
ClassCastException.
Error (như OutOfMemoryError) cũng unchecked nhưng ở nhánh riêng — biểu thị
hệ thống hỏng, không bao giờ bắt.
Cách phân biệt thực dụng: "người gọi có làm được gì có ích không?" Có → checked; không, chỉ có thể sửa code → unchecked.
try-with-resources hơn finally ở điểm nào?
Xem đáp án
Bốn điểm, điểm cuối là quan trọng nhất:
- Ngắn hơn nhiều — không cần khai báo biến ngoài, kiểm tra
null, vàtrylồng choclose(). - Không thể quên — trình biên dịch sinh code đóng tự động.
- Đóng đúng thứ tự — nhiều tài nguyên được đóng theo thứ tự ngược với thứ tự tạo, luôn đúng.
- ⭐ Không mất ngoại lệ gốc — nếu thân
tryném lỗi A vàclose()ném lỗi B, thì vớifinallythủ công lỗi B sẽ ghi đè lỗi A và bạn mất nguyên nhân thật. try-with-resources ném A ra ngoài và đính B vào dưới dạng suppressed exception, lấy được bằnge.getSuppressed().
Điều kiện: tài nguyên phải cài đặt AutoCloseable (hoặc Closeable). Từ
Java 9, dùng được cả biến effectively final đã khai báo trước: try (reader) { … }.
Đoạn này trả về gì? try { return 1; } finally { return 2; }
Xem đáp án
Trả về 2. finally luôn chạy trước khi phương thức thực sự trả về, và
return trong đó ghi đè giá trị đã chọn ở try.
Tệ hơn nhiều: nếu try đang ném ngoại lệ mà finally có
return, ngoại lệ bị nuốt hoàn toàn — chương trình chạy tiếp như không
có gì xảy ra:
try { throw new RuntimeException("nghiêm trọng"); }
finally { return 0; } // trả 0, ngoại lệ biến mất
Quy tắc cứng: finally chỉ chứa code dọn dẹp — không bao giờ có
return, break, continue, hay throw. Trong code
hiện đại, phần lớn nhu cầu của finally đã được try-with-resources thay thế.
Vì sao catch (Exception e) { } là anti-pattern tệ nhất?
Xem đáp án
Nó mắc hai lỗi cùng lúc:
- Nuốt ngoại lệ — lỗi biến mất không dấu vết. Chương trình chạy tiếp trong trạng thái hỏng, và triệu chứng xuất hiện ở một nơi hoàn toàn khác, rất lâu sau đó. Người debug không có manh mối nào.
- Bắt quá rộng —
Exceptionbao gồm cảRuntimeException, nên nó nuốt luônNullPointerExceptionvàClassCastException— tức là che giấu chính bug của bạn, thứ đáng lẽ phải lộ ra ngay để sửa.
Nếu thật sự cần bỏ qua một lỗi, hãy nêu rõ loại và lý do:
try {
tepTam.delete();
} catch (IOException e) {
log.debug("Không xoá được tệp tạm, hệ điều hành sẽ tự dọn", e);
}
Bắt Exception chỉ hợp lý ở biên chương trình — vòng lặp worker không
được chết vì một job hỏng, handler HTTP phải trả 500 thay vì sập server — và ngay cả ở đó vẫn phải
ghi log đầy đủ.
throw và throws khác nhau thế nào?
Xem đáp án
throw là câu lệnh — nó thực sự ném một object ngoại lệ, đặt
trong thân phương thức: throw new IOException("mất kết nối");. Mỗi lần chỉ ném một
object.
throws là mệnh đề khai báo trong chữ ký phương thức — nó nói
với người gọi rằng "phương thức này có thể ném những loại này":
void f() throws IOException, SQLException. Liệt kê được nhiều kiểu.
Chỉ checked exception mới cần khai báo trong throws. Unchecked ném
thoải mái mà không cần khai báo (dù vẫn nên ghi vào Javadoc bằng @throws để làm tài
liệu).
Ghi nhớ: throw là hành động, throws là lời hứa.
Vì sao nhiều ngôn ngữ hiện đại bỏ checked exception?
Xem đáp án
Ba vấn đề thực tế mà ba mươi năm sử dụng đã bộc lộ:
- "throws" lây lan — một
IOExceptionở tầng dưới cùng buộc mọi tầng phía trên phải khai báo lại, kể cả tầng giao diện vốn chẳng quan tâm chi tiết đó. Chi tiết cài đặt rò rỉ lên toàn bộ kiến trúc. - Khuyến khích nuốt lỗi — khi trình biên dịch ép viết
catch, người vội sẽ viết một khối rỗng. Kết quả còn tệ hơn là để lỗi bay lên. - Xung đột với lambda — các functional interface trong JDK (
Function,Consumer…) không khai báothrows, nên không viết được lambda ném checked exception. Điều này khiến Stream API rất khó dùng với I/O.
Vì vậy C# (2000) cố tình không có checked exception, và Kotlin, Scala, Groovy cũng vậy. Bản thân JDK
cũng đi theo hướng đó: Java 8 thêm UncheckedIOException.
Thực hành khuyến nghị: API mới dùng unchecked làm mặc định; bọc checked exception từ thư viện thành unchecked ở biên giữa tầng hạ tầng và tầng nghiệp vụ — và luôn giữ nguyên nhân gốc bằng chaining.
Bắt InterruptedException thì phải làm gì thêm? Vì sao?
Xem đáp án
Phải khôi phục cờ ngắt: Thread.currentThread().interrupt();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ⚠️ BẮT BUỘC
throw new RuntimeException("Bị ngắt khi đang chờ", e);
}
Lý do: khi InterruptedException được ném, JVM xoá cờ ngắt của
luồng. Nếu bạn bắt ngoại lệ mà không khôi phục cờ, tín hiệu "hãy dừng lại" bị mất hoàn toàn — các
tầng gọi phía trên không bao giờ biết có người đã yêu cầu luồng này dừng.
Hậu quả thực tế: ExecutorService.shutdownNow() không dừng được luồng, ứng dụng treo
khi tắt, container không kill được tiến trình một cách êm đẹp. Đây là một trong những bug đa luồng
phổ biến nhất và sẽ được đào sâu ở Chương 12.
"Caused by" trong stack trace nghĩa là gì? Cách đọc stack trace dài?
Xem đáp án
Caused by xuất hiện khi có exception chaining: bạn bắt một ngoại lệ
rồi ném một ngoại lệ khác, truyền cái cũ làm nguyên nhân
(new MyException("msg", e)). Java in cả chuỗi, từ ngoài vào trong.
Cách đọc:
- Nhảy xuống khối
Caused bycuối cùng — đó là nguyên nhân gốc thật sự. - Tìm dòng đầu tiên thuộc package của bạn — bỏ qua các frame của JDK và thư viện. Đó thường là nơi cần sửa.
... N morenghĩa là N frame còn lại trùng với stack trace phía trên, đã lược cho gọn.Suppressed:là ngoại lệ phụ xảy ra khi đóng tài nguyên (try-with-resources) — thường ít quan trọng hơn nhưng đôi khi hé lộ nguyên nhân gốc.
Điều này giải thích vì sao throw new MyException("msg") (không truyền cause) là sai:
nó cắt đứt chuỗi và bạn mất hoàn toàn thông tin về nơi lỗi thực sự phát sinh.
Tổng kết
Sau chương 6, bạn đã nắm:
- Cơ chế:
throwcắt ngang luồng, gỡ ngăn xếp tới khi gặpcatchkhớp. - Cây
Throwable:Error(đừng bắt) /RuntimeException(unchecked) / cácExceptionkhác (checked). - Checked vs unchecked và câu hỏi quyết định: "người gọi có làm được gì có ích không?"
try/catch/finally: cụ thể trước tổng quát sau, multi-catch, và cấmreturntrongfinally.- try-with-resources: ngắn hơn, đóng đúng thứ tự, và giữ được ngoại lệ gốc nhờ suppressed.
throwvsthrows: hành động vs lời hứa; quy tắc "catch or specify".- Custom exception: cây lỗi cho module, mang theo dữ liệu chứ không chỉ chuỗi, mặc định unchecked.
- Chaining: luôn truyền nguyên nhân; đọc stack trace từ
Caused bycuối cùng. - Sáu anti-pattern và cách sửa từng cái.
- Thực hành tốt: fail fast, thông điệp đủ ngữ cảnh,
Optionalcho "không tìm thấy", khôi phục cờ ngắt.
Kết nối
- Chương 5 (OOP nâng cao) — cây
Throwablelà ví dụ kế thừa lớn nhất JDK; quy tắc override không được mở rộng checked exception. - Chương 7-8 (Collections) —
ConcurrentModificationException,UnsupportedOperationException,NoSuchElementException. - Chương 10 (Stream & Optional) —
Optionalthay ngoại lệ cho "không tìm thấy"; vì sao lambda kỵ checked exception. - Chương 11 (Tệp tin) —
IOExceptionở khắp nơi; try-with-resources là bắt buộc. - Chương 12 (Đa luồng) —
InterruptedExceptionvà cờ ngắt; ngoại lệ trong luồng con không tự nổi lên luồng cha. - Chương 13 (JDBC) —
SQLException, rollback trongcatch, try-with-resources choConnection. - Chương 14 (Kiểm thử) —
assertThrows()của JUnit 5 để khẳng định đúng loại ngoại lệ và đúng thông điệp. - Python Chương 5 — đối chiếu: Python không có checked exception; có khối
elsemà Java không có;with≈ try-with-resources;raise … from …≈ chaining.