Chuyển tới nội dung chính

🎯 Ngoại Lệ Tùy Chỉnh - Custom Exceptions

🎯 Món Ăn Hôm Nay

Khi vận hành quán café, bạn sẽ gặp các vấn đề đặc thù riêng mà Java không có ngoại lệ sẵn: hết nguyên liệu, ngoài giờ phục vụ, khách hàng VIP yêu cầu đặc biệt... Hôm nay chúng ta học cách tạo ngoại lệ riêng cho quán của mình!

☕ Custom Exception Là Gì?

Custom Exception là ngoại lệ tự tạo để xử lý các tình huống nghiệp vụ đặc thù của ứng dụng.

Tưởng tượng:

  • Java có sẵn: FileNotFoundException, NullPointerException
  • Quán café cần: HetNguyenLieuException, NgoaiGioPhucVuException, KhongCoChoNgoiException

Giống như quán có quy định riêng, bạn tạo ngoại lệ riêng để xử lý các tình huống đặc biệt!

👨‍🍳 Câu Chuyện Trong Quán

Tình huống: Quán cần quản lý nhiều loại vấn đề

  1. Hết nguyên liệuHetNguyenLieuException
  2. Ngoài giờ phục vụNgoaiGioException
  3. Không đủ chỗ ngồiKhongCoChoException
  4. Món không có trong menuMonKhongTonTaiException

Thay vì dùng Exception chung chung, ta tạo ngoại lệ cụ thể cho từng tình huống!

📝 Công Thức Tạo Custom Exception

Ví Dụ 1: Exception Cơ Bản - Hết Nguyên Liệu

Tạo ngoại lệ đơn giản:

// 📦 Định nghĩa ngoại lệ riêng
public class HetNguyenLieuException extends Exception {
// Constructor với thông điệp
public HetNguyenLieuException(String nguyenLieu) {
super("❌ Hết " + nguyenLieu + "! Vui lòng chọn món khác.");
}
}

// 🏪 Sử dụng trong quán
public class QuanCafe {
static int soLuongSua = 0;

public static void phaLatte() throws HetNguyenLieuException {
if (soLuongSua <= 0) {
throw new HetNguyenLieuException("sữa");
}
System.out.println("☕ Pha Latte thành công!");
}

public static void main(String[] args) {
try {
phaLatte();
} catch (HetNguyenLieuException e) {
System.out.println(e.getMessage());
System.out.println("💡 Đề xuất: Thử Americano thay thế");
}
}
}

Output:

❌ Hết sữa! Vui lòng chọn món khác.
💡 Đề xuất: Thử Americano thay thế

Ví Dụ 2: Exception Với Thông Tin Chi Tiết

Lưu thêm thông tin:

public class NgoaiGioException extends Exception {
private int gioHienTai;
private int gioMoCua = 7;
private int gioDongCua = 22;

public NgoaiGioException(int gio) {
super("❌ Quán đóng cửa! Giờ mở cửa: 7h-22h");
this.gioHienTai = gio;
}

// Getter để lấy thông tin chi tiết
public int getGioHienTai() {
return gioHienTai;
}

public String getThongTinChiTiet() {
return String.format(
"Giờ hiện tại: %dh\nGiờ mở cửa: %dh-%dh",
gioHienTai, gioMoCua, gioDongCua
);
}
}

// Sử dụng
public static void kiemTraGioMoCua(int gio) throws NgoaiGioException {
if (gio < 7 || gio > 22) {
throw new NgoaiGioException(gio);
}
System.out.println("✅ Quán đang mở cửa!");
}

// Test
try {
kiemTraGioMoCua(23);
} catch (NgoaiGioException e) {
System.out.println(e.getMessage());
System.out.println(e.getThongTinChiTiet());
}

Output:

❌ Quán đóng cửa! Giờ mở cửa: 7h-22h
Giờ hiện tại: 23h
Giờ mở cửa: 7h-22h

Ví Dụ 3: Hệ Thống Ngoại Lệ Phân Cấp

Tạo exception cha và con:

// 🎯 Exception cha cho tất cả lỗi quán café
public class QuanCafeException extends Exception {
public QuanCafeException(String message) {
super("🏪 LỖI QUÁN CAFÉ: " + message);
}
}

// 📦 Exception con - Vấn đề nguyên liệu
public class NguyenLieuException extends QuanCafeException {
public NguyenLieuException(String nguyenLieu) {
super("Hết " + nguyenLieu);
}
}

// 👥 Exception con - Vấn đề khách hàng
public class KhachHangException extends QuanCafeException {
public KhachHangException(String message) {
super(message);
}
}

// ⏰ Exception con - Vấn đề thời gian
public class ThoiGianException extends QuanCafeException {
public ThoiGianException(String message) {
super(message);
}
}

// Sử dụng
public static void phucVuKhach(String mon, int gio)
throws QuanCafeException {

// Kiểm tra giờ
if (gio < 7 || gio > 22) {
throw new ThoiGianException("Ngoài giờ phục vụ");
}

// Kiểm tra nguyên liệu
if (mon.equals("Latte") && soLuongSua == 0) {
throw new NguyenLieuException("sữa");
}

System.out.println("✅ Phục vụ " + mon + " thành công!");
}

// Test với catch đa cấp
try {
phucVuKhach("Latte", 8);
} catch (NguyenLieuException e) {
System.out.println("📦 " + e.getMessage());
} catch (ThoiGianException e) {
System.out.println("⏰ " + e.getMessage());
} catch (QuanCafeException e) {
System.out.println("🏪 " + e.getMessage());
}

Ví Dụ 4: RuntimeException Tùy Chỉnh

Exception không bắt buộc try-catch:

// Extend RuntimeException thay vì Exception
public class GiaKhongHopLeException extends RuntimeException {
public GiaKhongHopLeException(int gia) {
super(String.format(
"❌ Giá %,d VND không hợp lệ! Giá phải từ 10,000-200,000 VND",
gia
));
}
}

// Sử dụng - KHÔNG BẮT BUỘC try-catch
public static void datGia(int gia) {
if (gia < 10000 || gia > 200000) {
throw new GiaKhongHopLeException(gia);
}
System.out.println("✅ Đặt giá: " + gia + " VND");
}

// Có thể gọi mà không cần try-catch
public static void main(String[] args) {
datGia(50000); // ✅ OK
datGia(5000); // ❌ Throw exception
}

Ví Dụ 5: Exception Với Mã Lỗi

Thêm error code để dễ xử lý:

public class QuanCafeException extends Exception {
private String errorCode;

public QuanCafeException(String code, String message) {
super(message);
this.errorCode = code;
}

public String getErrorCode() {
return errorCode;
}
}

// Định nghĩa mã lỗi
public class ErrorCode {
public static final String OUT_OF_STOCK = "E001";
public static final String CLOSED = "E002";
public static final String NO_SEAT = "E003";
public static final String INVALID_ITEM = "E004";
}

// Sử dụng
public static void phaDoUong(String mon) throws QuanCafeException {
if (mon.equals("Latte") && soLuongSua == 0) {
throw new QuanCafeException(
ErrorCode.OUT_OF_STOCK,
"Hết sữa - không thể pha Latte"
);
}
}

// Xử lý theo mã lỗi
try {
phaDoUong("Latte");
} catch (QuanCafeException e) {
System.out.println("Mã lỗi: " + e.getErrorCode());
System.out.println("Chi tiết: " + e.getMessage());

// Xử lý khác nhau theo error code
switch (e.getErrorCode()) {
case ErrorCode.OUT_OF_STOCK:
System.out.println("💡 Đề xuất món thay thế");
break;
case ErrorCode.CLOSED:
System.out.println("💡 Quay lại vào 7h sáng mai");
break;
}
}

Ví Dụ 6: Exception Với Cause Chain

Lưu lỗi gốc:

public class DonHangException extends Exception {
public DonHangException(String message, Throwable cause) {
super(message, cause);
}
}

public static void xuLyDonHang(String file) throws DonHangException {
try {
// Đọc đơn hàng từ file
BufferedReader reader = new BufferedReader(
new FileReader(file)
);
// ...
reader.close();

} catch (FileNotFoundException e) {
// Wrap lỗi gốc vào custom exception
throw new DonHangException(
"❌ Không tìm thấy file đơn hàng: " + file,
e // Lưu lỗi gốc
);
} catch (IOException e) {
throw new DonHangException(
"❌ Lỗi đọc file đơn hàng",
e
);
}
}

// Xử lý và xem cause
try {
xuLyDonHang("order.txt");
} catch (DonHangException e) {
System.out.println(e.getMessage());
System.out.println("Nguyên nhân: " + e.getCause());
}

🔥 Thực Hành Trong Quán

Bài Tập 1: Exception Kiểm Tra Số Lượng

Tạo SoLuongKhongHopLeException để kiểm tra số lượng đơn hàng.

public class SoLuongKhongHopLeException extends Exception {
private int soLuong;

public SoLuongKhongHopLeException(int soLuong) {
super("❌ Số lượng không hợp lệ: " + soLuong);
this.soLuong = soLuong;
}

public int getSoLuong() {
return soLuong;
}
}

// Sử dụng
public static void datMon(int soLuong)
throws SoLuongKhongHopLeException {
if (soLuong <= 0 || soLuong > 100) {
throw new SoLuongKhongHopLeException(soLuong);
}
System.out.println("✅ Đặt " + soLuong + " ly");
}

// Test
try {
datMon(150);
} catch (SoLuongKhongHopLeException e) {
System.out.println(e.getMessage());
System.out.println("Số lượng tối đa: 100 ly");
}

Bài Tập 2: Hệ Thống Exception Menu

Tạo exception cho vấn đề menu.

public class MenuException extends Exception {
public MenuException(String message) {
super("📋 LỖI MENU: " + message);
}
}

public class MonKhongTonTaiException extends MenuException {
public MonKhongTonTaiException(String mon) {
super("Món '" + mon + "' không có trong menu");
}
}

public class MonHetHangException extends MenuException {
public MonHetHangException(String mon) {
super("Món '" + mon + "' đã hết hàng hôm nay");
}
}

// Sử dụng
static ArrayList<String> menu = new ArrayList<>(
Arrays.asList("Latte", "Cappuccino", "Mocha")
);
static ArrayList<String> hetHang = new ArrayList<>(
Arrays.asList("Mocha")
);

public static void datMon(String mon) throws MenuException {
if (!menu.contains(mon)) {
throw new MonKhongTonTaiException(mon);
}
if (hetHang.contains(mon)) {
throw new MonHetHangException(mon);
}
System.out.println("✅ Đặt " + mon + " thành công!");
}

// Test
try {
datMon("Latte"); // ✅ OK
datMon("Mocha"); // ❌ Hết hàng
datMon("Espresso"); // ❌ Không có trong menu
} catch (MonHetHangException e) {
System.out.println(e.getMessage());
} catch (MonKhongTonTaiException e) {
System.out.println(e.getMessage());
} catch (MenuException e) {
System.out.println(e.getMessage());
}

Bài Tập 3: Exception Với Logging

Tạo exception tự động ghi log.

public class QuanCafeException extends Exception {
public QuanCafeException(String message) {
super(message);
// Tự động ghi log khi tạo exception
logError(message);
}

private void logError(String message) {
System.err.println("[" + java.time.LocalDateTime.now() + "] " + message);
}
}

// Sử dụng
try {
throw new QuanCafeException("Hết sữa");
} catch (QuanCafeException e) {
System.out.println("Thông báo khách: " + e.getMessage());
}

⚠️ Những Lỗi Đầu Bếp Thường Gặp

Lỗi 1: Tạo Exception Quá Nhiều

SAI:

// Tạo exception cho mọi thứ - quá mức cần thiết
public class CupEmptyException extends Exception {}
public class SpoonDirtyException extends Exception {}
public class TableWetException extends Exception {}
// ... 50 exceptions khác

ĐÚNG:

// Nhóm các lỗi liên quan
public class NguyenLieuException extends Exception {
// Xử lý tất cả vấn đề nguyên liệu
}

Lỗi 2: Quên Gọi super()

SAI:

public class MyException extends Exception {
public MyException(String message) {
// Quên gọi super()!
}
}

ĐÚNG:

public class MyException extends Exception {
public MyException(String message) {
super(message); // Bắt buộc!
}
}

Lỗi 3: Exception Không Có Thông Tin

SAI:

public class ErrorException extends Exception {
// Không có constructor, không có thông tin gì!
}

ĐÚNG:

public class ErrorException extends Exception {
public ErrorException(String details) {
super("Lỗi xảy ra: " + details);
}
}

Lỗi 4: Lạm Dụng RuntimeException

SAI:

// Mọi thứ đều extends RuntimeException
public class MoiThuException extends RuntimeException {
// Mất kiểm soát!
}

ĐÚNG:

// RuntimeException chỉ cho lỗi lập trình
public class InvalidParameterException extends RuntimeException {}

// Exception cho lỗi nghiệp vụ
public class BusinessException extends Exception {}

Lỗi 5: Catch Exception Cha Trước Con

SAI:

try {
// ...
} catch (QuanCafeException e) {
// Catch cha trước
} catch (NguyenLieuException e) {
// Code này không bao giờ chạy!
}

ĐÚNG:

try {
// ...
} catch (NguyenLieuException e) {
// Catch con trước
} catch (QuanCafeException e) {
// Catch cha sau
}

💡 Bí Quyết Của Barista

1. Khi Nào Tạo Custom Exception?

  • Xử lý nghiệp vụ đặc thù (hết hàng, ngoài giờ, VIP)
  • Cần thông tin chi tiết (error code, timestamp, user info)
  • Muốn phân loại lỗi rõ ràng
  • Tạo API dễ sử dụng cho developer khác

2. Đặt Tên Exception

// ✅ TỐT - Rõ ràng, mô tả vấn đề
HetNguyenLieuException
NgoaiGioPhucVuException
KhongCoChoNgoiException

// ❌ TỆ - Chung chung, không rõ
ErrorException
ProblemException
IssueException

3. Cấu Trúc Exception Tốt

public class QuanCafeException extends Exception {
private String errorCode; // Mã lỗi
private LocalDateTime time; // Thời gian
private String details; // Chi tiết

public QuanCafeException(String code, String message) {
super(message);
this.errorCode = code;
this.time = LocalDateTime.now();
}

// Getters
public String getErrorCode() { return errorCode; }
public LocalDateTime getTime() { return time; }

// Helper
public String getFullInfo() {
return String.format("[%s] %s: %s",
errorCode, time, getMessage());
}
}

4. Phân Cấp Hợp Lý

QuanCafeException (cha)
├── NguyenLieuException
│ ├── HetSuaException
│ └── HetCafeException
├── ThoiGianException
│ ├── NgoaiGioException
│ └── TatCaException
└── KhachHangException
├── KhongCoChoException
└── VIPException

5. Documentation

/**
* Exception ném khi hết nguyên liệu.
*
* @author Barista Team
* @since 1.0
*/
public class HetNguyenLieuException extends QuanCafeException {
/**
* Tạo exception với tên nguyên liệu.
* @param nguyenLieu Tên nguyên liệu hết hàng
*/
public HetNguyenLieuException(String nguyenLieu) {
super("E001", "Hết " + nguyenLieu);
}
}

🎓 Bạn Đã Học Được

  • Tạo Exception riêng: extends Exception hoặc RuntimeException
  • Constructor: Luôn gọi super(message)
  • Phân cấp: Tạo exception cha-con hợp lý
  • Thông tin chi tiết: Thêm fields, getters, helpers
  • Error code: Dễ phân loại và xử lý
  • Cause chain: Lưu lỗi gốc với super(message, cause)
  • Checked vs Unchecked: Chọn đúng loại cho từng trường hợp

☕ Món Tiếp Theo

Bài tiếp theo: 📝 Xử Lý Chuỗi - String

Học cách xử lý tên món, mô tả, ghi chú đơn hàng!


💡 Lời Khuyên Cuối: Custom exception là công cụ mạnh mẽ, nhưng đừng lạm dụng! Chỉ tạo khi thực sự cần thiết. Một exception tốt phải có tên rõ ràng, thông điệp hữu ích, và dễ dàng xử lý. Happy coding! 🎯☕