Giới thiệu Exception Handling
Sau bài này, bạn sẽ:
- Hiểu exception là gì và tại sao cần xử lý exception trong Java
- Phân biệt Error vs Exception và Checked vs Unchecked Exception
- Nắm vững exception hierarchy từ Throwable đến các exception classes cụ thể
- Nhận biết các exception phổ biến: NullPointerException, ArrayIndexOutOfBoundsException, IOException
- Hiểu hậu quả khi không handle exception và lợi ích khi handle đúng cách
Exception là gì?
Exception (ngoại lệ) là một sự kiện bất thường xảy ra trong quá trình thực thi chương trình, làm gián đoạn luồng thực thi bình thường của code. Khi một exception xảy ra, chương trình sẽ crash (ngừng hoạt động) nếu không được xử lý đúng cách.
- Tránh crash chương trình: Giúp ứng dụng vẫn hoạt động ngay cả khi có lỗi
- Tạo trải nghiệm người dùng tốt hơn: Hiển thị thông báo lỗi dễ hiểu thay vì crash
- Debug dễ dàng hơn: Biết chính xác lỗi xảy ra ở đâu và nguyên nhân gì
- Dọn dẹp tài nguyên: Đảm bảo đóng file, connection, etc. ngay cả khi có lỗi
- Tách biệt logic xử lý lỗi: Code sạch hơn, dễ maintain hơn
Exception Hierarchy
Java có một hệ thống phân cấp exception rõ ràng. Hãy tưởng tượng như phân loại sự cố giao thông:
- Throwable = "Sự cố giao thông" (tất cả các sự cố trên đường)
- Error = "Thiên tai / sập cầu" — bạn không xử lý được, chỉ có thể tránh đi đường đó
- Exception = "Sự cố do xe / người lái" — bạn có thể xử lý hoặc phòng tránh
- Checked Exception = "Hết xăng, nổ lốp" — bạn buộc phải mang theo xăng dự phòng / lốp sơ cua
- Unchecked Exception = "Vượt đèn đỏ, quên xi-nhan" — lỗi do bạn, sửa cách lái là hết
Object
|
Throwable
/ \
Error Exception
| |
(system-level) |
/ \
/ \
IOException RuntimeException
SQLException |
etc. |
|
NullPointerException
ArrayIndexOutOfBoundsException
ArithmeticException
ClassCastException
etc.
Chi tiết Hierarchy
-
Throwable: Root class của tất cả exception và error
- Có 2 subclass chính:
ErrorvàException
- Có 2 subclass chính:
-
Error: Lỗi nghiêm trọng ở system-level, không nên catch
OutOfMemoryError: Hết bộ nhớStackOverflowError: Stack bị tràn (thường do recursion vô hạn)VirtualMachineError: Lỗi JVM
-
Exception: Lỗi có thể xử lý được
- Checked Exception: Compiler bắt buộc phải xử lý
- Unchecked Exception (RuntimeException): Không bắt buộc xử lý
Checked vs Unchecked Exceptions
| Đặc điểm | Checked Exception | Unchecked Exception |
|---|---|---|
| Parent class | Exception (không phải RuntimeException) | RuntimeException |
| Bắt buộc xử lý | Có (compile-time) | Không |
| Khi nào xảy ra | Lỗi bên ngoài tầm kiểm soát (I/O, network, database) | Lỗi lập trình (logic bug, null reference) |
| Ví dụ | IOException, SQLException, ClassNotFoundException | NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException |
| Xử lý | Bắt buộc dùng try-catch hoặc throws | Tùy chọn xử lý |
| Prevention | Không thể prevent hoàn toàn | Có thể prevent bằng code tốt hơn |
Nếu method có thể throw checked exception mà không xử lý, phải khai báo trong method signature với throws:
public void readFile() throws IOException {
// code đọc file
}
Common Exceptions trong Java
1. NullPointerException (Unchecked)
Exception phổ biến nhất! Xảy ra khi truy cập method/field của object null.
String text = null;
System.out.println(text.length()); // ❌ NullPointerException
// Cách tránh:
if (text != null) {
System.out.println(text.length()); // ✅ Safe
}
// Hoặc dùng Optional (Java 8+)
Optional<String> optionalText = Optional.ofNullable(text);
optionalText.ifPresent(t -> System.out.println(t.length()));
2. ArrayIndexOutOfBoundsException (Unchecked)
Xảy ra khi truy cập index không tồn tại trong array.
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ❌ ArrayIndexOutOfBoundsException
// Cách tránh:
if (index >= 0 && index < numbers.length) {
System.out.println(numbers[index]); // ✅ Safe
}
3. ClassCastException (Unchecked)
Xảy ra khi cast object sang type không tương thích.
Object obj = "Hello";
Integer num = (Integer) obj; // ❌ ClassCastException
// Cách tránh:
if (obj instanceof Integer) {
Integer num = (Integer) obj; // ✅ Safe
}
4. ArithmeticException (Unchecked)
Xảy ra khi có phép toán không hợp lệ, ví dụ chia cho 0.
int result = 10 / 0; // ❌ ArithmeticException
// Cách tránh:
int divisor = 0;
if (divisor != 0) {
int result = 10 / divisor;
} else {
System.out.println("Cannot divide by zero");
}
double result = 10.0 / 0.0; // ✅ Không throw exception
System.out.println(result); // In ra: Infinity
Floating point division by zero không throw exception, trả về Infinity hoặc NaN.
5. IOException (Checked)
Xảy ra khi có lỗi I/O (đọc/ghi file, network, etc.).
// ❌ Code này không compile vì không xử lý IOException
public void readFile() {
FileReader reader = new FileReader("data.txt");
}
// ✅ Phải xử lý checked exception
public void readFile() throws IOException {
FileReader reader = new FileReader("data.txt");
}
// Hoặc dùng try-catch
public void readFile() {
try {
FileReader reader = new FileReader("data.txt");
} catch (IOException e) {
System.out.println("File not found: " + e.getMessage());
}
}
6. FileNotFoundException (Checked)
Subclass của IOException, xảy ra khi file không tồn tại.
try {
FileInputStream fis = new FileInputStream("nonexistent.txt");
} catch (FileNotFoundException e) {
System.out.println("File not found: " + e.getMessage());
}
Error vs Exception
Error
Error là lỗi nghiêm trọng ở system-level mà application không nên cố gắng catch. Đây là lỗi JVM hoặc hardware.
// ❌ KHÔNG NÊN catch Error
try {
// some code
} catch (Error e) {
// Bad practice!
}
Ví dụ Error phổ biến:
1. OutOfMemoryError
Xảy ra khi JVM hết heap memory.
// Code này sẽ throw OutOfMemoryError
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // Thêm 1MB mỗi lần
}
2. StackOverflowError
Xảy ra khi stack bị tràn, thường do recursion không có điều kiện dừng.
// ❌ Recursion vô hạn
public void infiniteRecursion() {
infiniteRecursion(); // Gọi chính nó mãi mãi
}
- Hết memory (OutOfMemoryError)
- Stack overflow (StackOverflowError)
- Lỗi JVM (VirtualMachineError)
- Lỗi link class (LinkageError)
Giải pháp: Fix root cause (tăng memory, fix logic), KHÔNG catch Error!
Exception
Exception là lỗi có thể xử lý được trong application code. Nên catch và xử lý phù hợp.
Ví dụ thực tế: Chương trình crash khi không handle exception
Ví dụ 1: Không handle exception
public class WithoutExceptionHandling {
public static void main(String[] args) {
System.out.println("Program started");
// ❌ Không xử lý exception
String text = null;
System.out.println(text.length()); // NullPointerException!
// Code sau đây sẽ KHÔNG BAO GIỜ chạy
System.out.println("Program ended"); // ❌ Never executed
}
}
Output:
Program started
Exception in thread "main" java.lang.NullPointerException
at WithoutExceptionHandling.main(WithoutExceptionHandling.java:6)
Chương trình crash tại dòng lỗi, code phía sau không chạy.
Ví dụ 2: Có handle exception
public class WithExceptionHandling {
public static void main(String[] args) {
System.out.println("Program started");
try {
// ✅ Xử lý exception trong try-catch
String text = null;
System.out.println(text.length());
} catch (NullPointerException e) {
System.out.println("Error: text is null!");
}
// Code này VẪN chạy bình thường
System.out.println("Program ended"); // ✅ Executed!
}
}
Output:
Program started
Error: text is null!
Program ended
Chương trình không crash, xử lý lỗi gracefully và tiếp tục chạy.
Ví dụ 3: Real-world scenario - Banking System
public class BankingSystem {
public static void main(String[] args) {
System.out.println("Banking System Started");
try {
// Giả lập rút tiền
withdraw(1000, 500); // ✅ OK: Rút 500 từ tài khoản có 1000
withdraw(200, 500); // ❌ ERROR: Số dư không đủ
} catch (ArithmeticException e) {
System.out.println("Transaction failed: " + e.getMessage());
// Log error, gửi notification, etc.
}
System.out.println("Banking System Still Running"); // ✅ System vẫn chạy
}
public static void withdraw(int balance, int amount) {
if (amount > balance) {
throw new ArithmeticException("Insufficient balance");
}
System.out.println("Withdrawal successful: " + amount);
}
}
Output:
Banking System Started
Withdrawal successful: 500
Transaction failed: Insufficient balance
Banking System Still Running
- Không xử lý exception → Chương trình crash, mất dữ liệu, trải nghiệm xấu
- Có xử lý exception → Chương trình ổn định, xử lý lỗi gracefully, log được lỗi
- Exception handling là bắt buộc cho production code!
Tại sao Java có Checked Exceptions?
Nhiều ngôn ngữ hiện đại (C#, Kotlin, Python) không có checked exceptions — tất cả đều là unchecked. Vậy tại sao Java lại thiết kế khác?
Triết lý thiết kế
Java ra đời năm 1995 với triết lý: "Buộc developer phải nghĩ về lỗi ngay khi viết code". Checked exceptions là cách compiler nhắc bạn: "Hé, operation này có thể fail — bạn định làm gì nếu nó fail?"
// Compiler nhắc: "File có thể không tồn tại, bạn xử lý chưa?"
public void readConfig() throws IOException {
FileReader reader = new FileReader("config.txt");
// ...
}
Ưu và nhược điểm
| Checked Exception | Unchecked Exception | |
|---|---|---|
| Ưu điểm | Compiler nhắc xử lý lỗi, API rõ ràng hơn | Code gọn hơn, linh hoạt hơn |
| Nhược điểm | Verbose, "throws pollution" lan khắp call chain | Dễ quên xử lý, runtime mới biết |
Khi nào dùng loại nào?
- Checked: Khi caller có thể và nên recover — I/O errors, network timeouts, business rule violations
- Unchecked: Khi lỗi là bug lập trình cần fix code — null reference, invalid argument, array out of bounds
Spring Framework, Hibernate, và nhiều thư viện Java hiện đại prefer unchecked exceptions. Spring wrap hầu hết checked exceptions (như SQLException) thành unchecked (DataAccessException). Lý do: để developer chọn xử lý thay vì bị buộc xử lý.
Exception Propagation — Lan truyền Exception
Khi một exception xảy ra, nó "bay lên" qua call stack cho đến khi gặp catch block phù hợp. Nếu không có catch nào bắt được, chương trình crash.
main() ← 4. Nếu không ai bắt → CRASH!
│
▼
processOrder() ← 3. Không catch → tiếp tục bay lên
│
▼
validatePayment() ← 2. Không catch → bay lên caller
│
▼
chargeCard() ← 1. Exception xảy ra tại đây!
💥 PaymentException
public static void main(String[] args) {
try {
processOrder(); // Bắt exception ở đây
} catch (Exception e) {
System.out.println("Order failed: " + e.getMessage());
}
}
static void processOrder() {
validatePayment(); // Không catch, exception bay lên main()
}
static void validatePayment() {
chargeCard(); // Không catch, exception bay lên processOrder()
}
static void chargeCard() {
throw new RuntimeException("Card declined"); // Exception bắt đầu từ đây
}
- Unchecked exception: Tự động propagate, không cần khai báo
throws - Checked exception: Phải khai báo
throwshoặc catch — nếu không, compile error
Exception trong Method Overriding
Khi override method ở subclass, có quy tắc nghiêm ngặt về exception:
Method con KHÔNG THỂ ném broader checked exception hơn method cha.
Đây là hệ quả của Liskov Substitution Principle (LSP): nếu bạn dùng biến kiểu Parent nhưng object thực tế là Child, code xử lý exception của Parent phải vẫn đủ để bắt hết exception từ Child.
6 Scenarios — OK và FAIL
class Parent {
void doWork() throws IOException { }
}
| # | Child override | Kết quả | Lý do |
|---|---|---|---|
| 1 | void doWork() throws IOException | ✅ OK | Cùng exception |
| 2 | void doWork() throws FileNotFoundException | ✅ OK | Narrower (FileNotFoundException extends IOException) |
| 3 | void doWork() | ✅ OK | Không throw gì cũng được |
| 4 | void doWork() throws Exception | ❌ FAIL | Broader hơn IOException |
| 5 | void doWork() throws IOException, SQLException | ❌ FAIL | Thêm checked exception mới |
| 6 | void doWork() throws IOException, ArithmeticException | ✅ OK | Unchecked exception không bị giới hạn |
class Child extends Parent {
// ✅ OK: narrower exception
@Override
void doWork() throws FileNotFoundException {
throw new FileNotFoundException("not found");
}
}
class BadChild extends Parent {
// ❌ COMPILE ERROR: Exception is broader than IOException
@Override
void doWork() throws Exception { // Compile error!
throw new Exception("oops");
}
}
- Checked exception: Method con chỉ được throw same hoặc narrower (hoặc không throw gì)
- Unchecked exception: Thoải mái — không bị giới hạn bởi rule này
- Rule này chỉ áp dụng khi override (cùng signature), không áp dụng khi overload
Tóm tắt
| Khái niệm | Mô tả |
|---|---|
| Exception | Sự kiện bất thường làm gián đoạn luồng thực thi |
| Error | Lỗi nghiêm trọng ở system-level, không nên catch |
| Checked Exception | Bắt buộc xử lý tại compile-time (IOException, SQLException) |
| Unchecked Exception | Không bắt buộc xử lý (RuntimeException, NullPointerException) |
| try-catch | Cơ chế xử lý exception (sẽ học ở bài tiếp theo) |
| Exception Propagation | Exception "bay lên" call stack cho đến khi được catch |
| Overriding Rule | Method con không thể throw broader checked exception |
Bài tập
-
Bài 1: Viết chương trình chia 2 số nhập từ bàn phím. Chương trình crash khi nhập số chia = 0. Quan sát và ghi lại exception nào được throw.
-
Bài 2: Tạo một array có 5 phần tử. Viết code cố gắng truy cập phần tử thứ 10. Quan sát exception.
-
Bài 3: Giải thích tại sao code sau compile error:
public void readFile() {
FileReader reader = new FileReader("data.txt");
}
-
Bài 4: Phân biệt:
NullPointerExceptionvsFileNotFoundExceptionErrorvsExceptionCheckedvsUncheckedexception
-
Bài 5: Viết method
parseInt(String str)convert String sang int. Xử lý trường hợp String không phải là số hợp lệ (sử dụngInteger.parseInt()- nó throwNumberFormatException).
Trong bài tiếp theo, chúng ta sẽ học cách xử lý exception với try-catch-finally và try-with-resources!