1. Giới thiệu về khối finally
Khi bạn làm việc với tài nguyên — tệp, kết nối mạng, cơ sở dữ liệu — điều quan trọng là phải đảm bảo chúng được đóng hoặc giải phóng luôn luôn, ngay cả khi có lỗi phát sinh trong quá trình chạy. Trong Java có một khối chuyên dụng cho việc này — finally.
finally hoạt động như thế nào?
Khối finally là một phần của cấu trúc try-catch-finally. Mã bên trong finally luôn được thực thi (nếu có viết) — bất kể có ngoại lệ hay không. Ngay cả khi trong try có return hoặc ném ra ngoại lệ — finally vẫn sẽ chạy (trừ khi bạn tắt máy tính hoặc kết thúc chương trình cưỡng bức bằng System.exit(0)).
Cú pháp:
try {
// Mã có thể phát sinh ngoại lệ
} catch (ExceptionType e) {
// Xử lý lỗi
} finally {
// Đoạn mã này sẽ luôn được chạy!
}
Ví dụ
try {
System.out.println("Bắt đầu chạy");
int result = 10 / 0; // lỗi sẽ xảy ra ở đây
System.out.println("Kết quả: " + result);
} catch (ArithmeticException e) {
System.out.println("Lỗi: chia cho 0");
} finally {
System.out.println("Đoạn mã này sẽ luôn được chạy");
}
Kết quả thực thi:
Bắt đầu chạy
Lỗi: chia cho 0
Đoạn mã này sẽ luôn được chạy
Chuyện gì xảy ra?
- Trong try chúng ta thử chia hai số và gặp lỗi.
- Nếu khi chia phát sinh lỗi — catch sẽ xử lý nó.
- Nhưng! Dù thế nào finally cũng sẽ chạy và in thông báo ra console.
2. finally không có catch
Có 3 biến thể cấu trúc:
- Đầy đủ: try-catch-finally
- Không có finally: try-catch
- Không có catch: try-finally
Biến thể thứ ba được dùng khi phương thức ở tầng cao hơn sẽ bắt và xử lý lỗi. Nhưng khối finally là cần thiết để đảm bảo thực thi một đoạn mã nhất định:
- Đóng tệp, kết nối mạng, cơ sở dữ liệu.
- Giải phóng mọi tài nguyên (ví dụ, khóa).
- Ghi log: ghi lại thông tin về việc kết thúc thao tác.
Ví dụ:
try {
System.out.println("Chia số");
int result = 10 / 0; // lỗi!
System.out.println("Kết quả: " + result);
} finally {
System.out.println("Khối finally đã được chạy");
}
Kết quả:
Chia số
Khối finally đã được chạy
Exception in thread "main" java.lang.ArithmeticException: / by zero
Khi nào finally KHÔNG chạy?
Gần như luôn chạy. Ngoại lệ — nếu:
- Cưỡng bức kết thúc chương trình bằng: System.exit(0).
- Cưỡng bức “kill” luồng đang thực thi finally.
- Tắt máy tính.
3. Toán tử throw: tự ném ngoại lệ
Đôi khi Java tự “ném” ngoại lệ (ví dụ, chia cho 0, truy cập vượt giới hạn mảng). Nhưng cũng có tình huống khi bạn muốn nói rõ: “Đây là lỗi! Tôi không thể tiếp tục!”. Để làm điều đó trong Java có toán tử throw.
Tương tự: Nếu bạn ở cửa hàng và thấy hàng hóa hết hạn — bạn “ném” một khiếu nại. Trong code cũng vậy: nếu có gì đó sai — hãy ném ngoại lệ.
Cú pháp của throw
throw new ExceptionType("Thông báo lỗi");
ExceptionType — bất kỳ lớp nào kế thừa từ Throwable (thường là Exception hoặc RuntimeException). Trong ngoặc là thông báo giúp hiểu điều gì đã sai.
Ví dụ: kiểm tra tham số phương thức
public static int safeDivide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Số chia không thể bằng 0");
}
return a / b;
}
Cách dùng:
public static void main(String[] args) {
try {
int result = safeDivide(10, 0);
System.out.println("Kết quả: " + result);
} catch (IllegalArgumentException e) {
System.out.println("Lỗi: " + e.getMessage());
}
}
Kết quả:
Lỗi: Số chia không thể bằng 0
Khi nào dùng throw?
- Kiểm tra tham số phương thức (ví dụ, nếu nhận null hoặc dữ liệu không hợp lệ).
- Kiểm tra trạng thái đối tượng (ví dụ, cố gắng rút tiền từ tài khoản có 0 euro).
- Bên trong catch — nếu bạn muốn “ném tiếp” ngoại lệ ra ngoài (ví dụ, bổ sung thông tin).
4. Kết hợp try-catch-finally và throw
Đôi khi các cấu trúc này phối hợp với nhau. Ví dụ, bạn bắt một lỗi, nhưng sau đó quyết định ném ra lỗi khác giàu thông tin hơn.
public static int parseAndDivide(String text, int divisor) {
try {
int number = Integer.parseInt(text);
if (divisor == 0) {
throw new IllegalArgumentException("Số chia không thể bằng 0");
}
return number / divisor;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Chuỗi '" + text + "' không phải là số");
} finally {
System.out.println("Thử xử lý chuỗi: " + text);
}
}
Cách dùng:
try {
int result = parseAndDivide("42a", 2);
System.out.println("Kết quả: " + result);
} catch (IllegalArgumentException e) {
System.out.println("Lỗi: " + e.getMessage());
}
Kết quả:
Thử xử lý chuỗi: 42a
Lỗi: Chuỗi '42a' không phải là số
Điểm quan trọng: return và finally
Ngay cả khi trong khối try có return, finally vẫn sẽ chạy!
public static int getValue() {
try {
return 10;
} finally {
System.out.println("finally vẫn sẽ chạy!");
}
}
Gọi getValue() sẽ in:
finally vẫn sẽ chạy!
5. Những lỗi thường gặp khi dùng finally và throw
Lỗi số 1: quên đóng tài nguyên khi không có finally.
Rất thường gặp: mở tệp nhưng không đóng — dẫn đến rò rỉ tài nguyên. Luôn sử dụng finally (hoặc try-with-resources, mà chúng ta sẽ bàn sau).
Lỗi số 2: ném ngoại lệ nhưng không xử lý.
Nếu bạn ném ngoại lệ bằng throw nhưng không bắt nó ở đâu cả (không có try-catch), chương trình sẽ kết thúc đột ngột. Luôn nghĩ xem ai sẽ bắt ngoại lệ của bạn.
Lỗi số 3: return trong finally.
Nếu bạn vô tình viết return bên trong finally, nó sẽ “ghi đè” mọi return hoặc throw trước đó. Điều này có thể dẫn tới các bug rất khó phát hiện. Tuyệt đối không nên làm vậy!
public int tricky() {
try {
return 1;
} finally {
return 2; // NGUY HIỂM: trả về 2 chứ không phải 1!
}
}
Kết quả: sẽ trả về 2, dù trong try là 1.
Lỗi số 4: mất thông tin về ngoại lệ.
Nếu bạn bắt một ngoại lệ rồi ném ngoại lệ mới mà không giữ thông tin về ngoại lệ cũ (e), bạn sẽ mất stack trace, gây khó khăn cho việc gỡ lỗi. Tốt hơn hãy viết như sau:
catch (NumberFormatException e) {
throw new IllegalArgumentException("Lỗi chuyển đổi", e);
}
GO TO FULL VERSION