1. Lớp Throwable: gốc của mọi ngoại lệ
Bây giờ chúng ta sẽ phân tích chi tiết cách hệ thống ngoại lệ trong Java được tổ chức: Throwable là gì, Exception và Error khác nhau ra sao, và “đã kiểm tra” và “không kiểm tra” nghĩa là gì. Đây là nền tảng để xử lý lỗi một cách đúng đắn trong các chương trình của bạn.
Trong Java, tất cả ngoại lệ và lỗi đều là các đối tượng kế thừa từ lớp java.lang.Throwable.
Throwable — là “tổ tiên” của toàn bộ hệ phân cấp xử lý sự cố trong Java.
Sơ đồ khái quát:
Throwable
├── Exception
└── Error
Throwable — lớp cơ sở cho mọi thứ có thể được “ném” (throw) và “bắt” (catch) trong Java. Không thể sử dụng trực tiếp — nó đóng vai trò nền tảng cho các kiểu lỗi cụ thể hơn.
Exception — dành cho “lỗi bình thường”
Exception — lớp cơ sở cho các ngoại lệ có thể phát sinh trong chương trình và nên và cần được xử lý. Đây là các lỗi “trong công việc”: vấn đề với tệp, mạng, I/O, lỗi người dùng, v.v. Phần lớn các khối try-catch của bạn sẽ làm việc với các lớp con của Exception.
Ví dụ:
- IOException — lỗi khi làm việc với tệp hoặc mạng.
- SQLException — lỗi khi làm việc với cơ sở dữ liệu.
- FileNotFoundException — không tìm thấy tệp.
Error — dành cho lỗi chí mạng của JVM
Error — lớp cơ sở cho các lỗi xảy ra ở mức máy ảo Java (JVM). Thường là các sự cố nghiêm trọng mà chương trình không thể và không nên xử lý. Nếu xuất hiện Error — nhiều khả năng ứng dụng không thể tiếp tục chạy.
Ví dụ:
- OutOfMemoryError — hết bộ nhớ.
- StackOverflowError — tràn stack (ví dụ do đệ quy vô hạn).
- NoClassDefFoundError — không tìm thấy lớp cần thiết.
Quan trọng:
Bắt và xử lý Error — hầu như luôn là ý tưởng tệ. Đây không phải lỗi của chương trình bạn, mà là sự cố của môi trường thực thi.
2. Checked vs Unchecked exceptions: nghĩa là gì?
Trong Java, tất cả ngoại lệ được chia thành hai nhóm lớn:
Ngoại lệ đã kiểm tra (Checked)
Đó là gì? Các ngoại lệ mà trình biên dịch bắt buộc bạn phải xử lý hoặc khai báo ném tiếp.
Khi nào xảy ra? Thường liên quan đến tài nguyên bên ngoài: tệp, mạng, cơ sở dữ liệu, đầu vào của người dùng.
Xử lý thế nào? Hoặc bao bọc mã trong try-catch, hoặc thêm throws vào chữ ký phương thức.
Ví dụ: IOException, SQLException, FileNotFoundException
Ví dụ:
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path); // có thể ném IOException
// ...
}
Nếu không xử lý hoặc không khai báo ném tiếp — chương trình sẽ không biên dịch!
Ngoại lệ không kiểm tra (Unchecked)
Đó là gì? Các ngoại lệ không yêu cầu bắt buộc xử lý bởi trình biên dịch.
Khi nào xảy ra? Thường là lỗi trong logic chương trình: chia cho 0, vượt quá chỉ số mảng, truy cập null.
Xử lý thế nào? Có thể bắt nhưng không bắt buộc. Tốt hơn là phòng tránh bằng các kiểm tra hợp lệ.
Vị trí trong hệ phân cấp? Tất cả kế thừa từ RuntimeException.
Ví dụ: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException, ArithmeticException
Ví dụ:
int[] arr = {1, 2, 3};
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
Trình biên dịch không bắt buộc bạn phải bắt ngoại lệ này — nhưng chương trình sẽ “sập” khi lỗi xảy ra.
3. Toàn bộ hệ phân cấp trong một hình
graph TD
Throwable --> Error
Throwable --> Exception
Exception --> RuntimeException
Exception --> CheckedExceptions["(các Checked Exception khác)"]
Error --> OutOfMemoryError
Error --> StackOverflowError
RuntimeException --> NullPointerException
RuntimeException --> IndexOutOfBoundsException
RuntimeException --> IllegalArgumentException
%% Kiểu dáng
style Throwable fill:#ffa64d,color:#000
style Exception fill:#ffa64d,color:#000
style CheckedExceptions fill:#ffa64d,color:#000
style Error fill:#ff4d4d,color:#fff
style OutOfMemoryError fill:#ff4d4d,color:#fff
style StackOverflowError fill:#ff4d4d,color:#fff
style RuntimeException fill:#4dff88,color:#000
style NullPointerException fill:#4dff88,color:#000
style IndexOutOfBoundsException fill:#4dff88,color:#000
style IllegalArgumentException fill:#4dff88,color:#000
Bảng: khác biệt chính
| Nhóm | Lớp cha | Bắt buộc xử lý? | Ví dụ |
|---|---|---|---|
| Checked Exception | |
Có | |
| Unchecked | |
Không | |
| Error | |
Không | |
4. Trông như thế nào trong mã?
Checked Exception: ví dụ với tệp
import java.io.*;
public class FileDemo {
public static void main(String[] args) {
try {
FileReader reader = new FileReader("nofile.txt"); // FileNotFoundException (checked)
int data = reader.read();
System.out.println(data);
reader.close();
} catch (IOException e) {
System.out.println("Lỗi khi làm việc với tệp: " + e.getMessage());
}
}
}
Trình biên dịch sẽ buộc bạn xử lý IOException!
Unchecked Exception: ví dụ chia cho 0
public class ExceptionDemo {
public static void main(String[] args) {
int a = 10;
int b = 0;
int c = a / b; // ArithmeticException (unchecked)
System.out.println("Kết quả: " + c);
}
}
Trình biên dịch không yêu cầu xử lý, nhưng chương trình sẽ sập.
5. Tại sao cần hệ phân cấp ngoại lệ?
- Linh hoạt trong xử lý: Có thể bắt lỗi cụ thể (FileNotFoundException) hoặc cả một nhóm (IOException hoặc Exception).
- Tái sử dụng mã: Có thể xử lý tập trung các lỗi cùng loại.
- Sạch mã: Logic chính không bị lộn xộn bởi quá nhiều kiểm tra vụn vặt.
Ví dụ:
try {
// mã nguy hiểm
} catch (FileNotFoundException e) {
System.out.println("Không tìm thấy tệp!");
} catch (IOException e) {
System.out.println("Lỗi nhập/xuất!");
} catch (Exception e) {
System.out.println("Đã có lỗi xảy ra: " + e.getMessage());
}
6. Các lỗi thường gặp khi làm việc với ngoại lệ
Lỗi № 1: Bỏ qua ngoại lệ. Viết catch (Exception e) {} — không tốt! Bạn sẽ mất thông tin về nguyên nhân lỗi.
Lỗi № 2: Bắt quá nhiều. catch (Exception e) bắt mọi thứ, kể cả những gì bạn không ngờ tới. Tốt hơn hãy chỉ bắt những ngoại lệ mà bạn có thể xử lý.
Lỗi № 3: Bắt lỗi (Error). Không nên bắt Error nếu bạn không viết mã mức thấp. Đó là vấn đề của JVM, không phải của chương trình bạn.
Lỗi № 4: Không phân biệt checked và unchecked. Không phải mọi ngoại lệ đều như nhau! Checked yêu cầu xử lý (Exception), unchecked — không (RuntimeException và các lớp con).
Lỗi № 5: Không thêm thông tin vào ngoại lệ. Nếu bạn tạo ngoại lệ của riêng mình — hãy luôn thêm thông điệp có tính thông tin.
GO TO FULL VERSION