1. Giới thiệu
Trong thư viện chuẩn của Java đã có rất nhiều ngoại lệ: NullPointerException, IllegalArgumentException, IOException và các loại khác. Tuy nhiên đôi khi các ngoại lệ chuẩn là chưa đủ để mô tả rõ ràng lỗi xuất hiện trong chương trình của bạn.
Ví dụ thực tế:
Bạn viết một ứng dụng ngân hàng. Người dùng cố gắng rút nhiều tiền hơn số dư tài khoản. Bạn có thể ném IllegalArgumentException. Nhưng sẽ dễ hiểu hơn nhiều nếu dùng ngoại lệ của riêng bạn, ví dụ InsufficientFundsException. Khi đó nhìn vào mã là thấy ngay chuyện gì đã xảy ra.
Ngoại lệ tùy chỉnh là một dạng “tùy biến đúng cách” cho ứng dụng. Chúng cho phép tinh chỉnh cách xử lý sự cố, và qua tên gọi (nếu đặt tên hợp lý!) là có thể hiểu ngay điều gì đã xảy ra. Ngoài ra, chúng mang tính tự mô tả: các phương thức có throws MyException trong chữ ký sẽ nói ngay những lỗi nào có thể phát sinh. Hơn nữa — bạn luôn có thể thêm các trường bổ sung (ví dụ số dư, số tiền giao dịch, v.v.).
2. Cách tạo ngoại lệ của riêng bạn?
Rất đơn giản: tạo một lớp mới kế thừa từ một trong các lớp ngoại lệ chuẩn.
- Với ngoại lệ kiểm tra (checked) — kế thừa từ Exception.
- Với ngoại lệ không kiểm tra (unchecked) — kế thừa từ RuntimeException.
Ví dụ: ngoại lệ kiểm tra
public class InvalidCredentialsException extends Exception {
public InvalidCredentialsException(String message) {
super(message); // Truyền message lên lớp cha
}
}
Bây giờ bạn có thể ném ngoại lệ này trong mã của mình:
if (!login.equals("admin") || !password.equals("1234")) {
throw new InvalidCredentialsException("Sai tên đăng nhập hoặc mật khẩu");
}
Ví dụ: ngoại lệ không kiểm tra
public class NegativeBalanceException extends RuntimeException {
public NegativeBalanceException(String message) {
super(message);
}
}
Khi nào dùng checked, khi nào dùng unchecked?
- Checked — nếu lỗi dự kiến và có thể xử lý được (ví dụ lỗi xác thực, thiếu tệp, dữ liệu người dùng không hợp lệ).
- Unchecked — nếu lỗi liên quan đến bug trong logic chương trình (ví dụ chia cho 0, vi phạm bất biến).
3. Constructor: làm cho ngoại lệ giàu thông tin
Thường trong lớp ngoại lệ của bạn sẽ có ít nhất một constructor với tham số String message. Tuy nhiên, người ta cũng thường thêm các constructor khác:
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException() {
super();
}
public ScoreLimitExceededException(String message) {
super(message);
}
public ScoreLimitExceededException(String message, Throwable cause) {
super(message, cause);
}
public ScoreLimitExceededException(Throwable cause) {
super(cause);
}
}
Giải thích:
- message — mô tả văn bản của lỗi.
- cause — nguyên nhân (một ngoại lệ khác), nếu bạn muốn “bọc” một lỗi trong một lỗi khác.
Gợi ý: nếu chưa rõ cần những constructor nào — hãy thêm ít nhất constructor nhận một chuỗi.
4. Sử dụng ngoại lệ tùy chỉnh trong mã
Hãy xem một ví dụ: chúng ta có một người dùng, người dùng có điểm, và không được phép thêm quá 100.
public class User {
private String name;
private int score;
public User(String name) {
this.name = name;
this.score = 0;
}
public void addScore(int points) throws ScoreLimitExceededException {
if (score + points > 100) {
throw new ScoreLimitExceededException("Vượt quá giới hạn điểm! Cố gắng thêm: " + points);
}
this.score += points;
}
}
Lớp ngoại lệ:
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException(String message) {
super(message);
}
}
Xử lý:
try {
user.addScore(60);
user.addScore(50); // Ngoại lệ sẽ được ném tại đây!
} catch (ScoreLimitExceededException e) {
System.out.println("Lỗi: " + e.getMessage());
}
Kết quả:
Lỗi: Vượt quá giới hạn điểm! Cố gắng thêm: 50
Có thể bạn sẽ hỏi: nếu thay vì ngoại lệ chỉ dùng điều kiện if và, chẳng hạn, trả về false hoặc một giá trị đặc biệt khác để biểu thị thao tác thất bại thì sao? Ví dụ:
public boolean addScore(int points) {
if (score + points > 100) {
return false; // Hoặc ném một RuntimeException nào đó nếu không muốn xử lý
}
this.score += points;
return true;
}
Mặc dù cách làm này có vẻ đơn giản hơn, nó có nhược điểm khi nói đến các lỗi nghiêm trọng hoặc vi phạm logic ứng dụng.
Thứ nhất, việc trả về false hoặc một giá trị nào đó để biểu thị lỗi buộc mã gọi phải luôn kiểm tra giá trị trả về. Nếu lập trình viên quên làm điều này, lỗi có thể bị bỏ sót, dẫn đến hành vi khó lường. Ngược lại, ngoại lệ buộc phải xử lý (đối với ngoại lệ kiểm tra) hoặc ít nhất báo hiệu rõ ràng về vấn đề nếu chúng không được bắt.
Thứ hai, ngoại lệ truyền tải ngữ nghĩa lỗi rõ ràng hơn. Việc trả về false có thể mang nhiều nghĩa: “không thành công”, “không áp dụng”, “không khả dụng”. Ngoại lệ ScoreLimitExceededException nói một cách rõ ràng và dứt khoát: “đã vượt quá giới hạn điểm”. Điều này cải thiện khả năng đọc và bảo trì mã.
Thứ ba, ngoại lệ cho phép tập trung hóa xử lý lỗi. Thay vì rải các kiểm tra if khắp nơi gọi addScore, bạn có thể bắt ngoại lệ tại một chỗ và đưa ra quyết định phù hợp: hiển thị thông báo cho người dùng, ghi log, hoặc thực hiện hoàn tác giao dịch (rollback).
Cuối cùng, trong các vấn đề như vượt giới hạn, đây thực sự là tình huống ngoại lệ (nên mới gọi là “ngoại lệ”). Luồng thực thi bình thường giả định rằng điểm sẽ được cộng thành công. Nếu không phải vậy — đó là vi phạm logic nghiệp vụ hoặc các bất biến của đối tượng, và đây là kịch bản lý tưởng để dùng ngoại lệ.
5. Thêm các trường tùy chỉnh vào ngoại lệ
Đôi khi hữu ích khi thêm vào ngoại lệ của bạn các dữ liệu bổ sung giúp xử lý lỗi.
Ví dụ:
public class ScoreLimitExceededException extends Exception {
private int currentScore;
private int attemptedAdd;
public ScoreLimitExceededException(String message, int currentScore, int attemptedAdd) {
super(message);
this.currentScore = currentScore;
this.attemptedAdd = attemptedAdd;
}
public int getCurrentScore() {
return currentScore;
}
public int getAttemptedAdd() {
return attemptedAdd;
}
}
Cách dùng:
if (score + points > 100) {
throw new ScoreLimitExceededException(
"Vượt quá giới hạn điểm!",
this.score,
points
);
}
6. Những lưu ý hữu ích
Đặt tên ngoại lệ như thế nào?
Trong Java, thông lệ là đặt tên ngoại lệ tùy chỉnh với hậu tố Exception: InvalidUserInputException, InsufficientFundsException, ScoreLimitExceededException.
Đừng đặt tên ngoại lệ chỉ là Error hoặc Warning — điều đó có thể gây hiểu nhầm cho các lập trình viên khác (và chính bạn sau vài tuần).
Nên ném ngoại lệ ở đâu và khi nào?
- Khi xác thực dữ liệu người dùng (ví dụ: tên trống, tuổi âm).
- Khi vi phạm quy tắc nghiệp vụ (ví dụ: vượt quá giới hạn, cố gắng rút nhiều tiền hơn số dư).
- Khi gặp lỗi khi làm việc với dịch vụ bên ngoài (ví dụ: dịch vụ không khả dụng, hết thời gian chờ).
7. Các lỗi thường gặp khi tạo ngoại lệ tùy chỉnh
Lỗi số 1: Kế thừa sai lớp.
Hãy kế thừa từ Exception (hoặc RuntimeException), chứ không phải từ Throwable hay Error.
Lỗi số 2: Không thêm constructor có message.
Nếu không có constructor nhận chuỗi (String message), ngoại lệ của bạn sẽ “câm” và khó gỡ lỗi.
Lỗi số 3: Dùng ngoại lệ chuẩn cho logic nghiệp vụ.
Không nên ném NullPointerException hoặc IllegalArgumentException ở nơi cần một ngoại lệ “có ý nghĩa” riêng.
Lỗi số 4: Lạm dụng ngoại lệ tùy chỉnh.
Không nên tạo một lớp ngoại lệ riêng cho từng chuyện nhỏ nhặt. Nếu lỗi không đặc thù cho miền bài toán của bạn, hãy dùng ngoại lệ chuẩn.
Lỗi số 5: Thiếu tuần tự hóa (hiếm nhưng có).
Nếu ngoại lệ của bạn sẽ được truyền qua mạng hoặc lưu trữ, nên triển khai implements Serializable. Tuy nhiên, với các ứng dụng đơn giản thì điều này không quá quan trọng.
GO TO FULL VERSION