Java 自訂例外

當內建的例外類別無法精確描述錯誤情況時,可以建立自訂例外類別。

建立自訂例外

Checked Exception

繼承 Exception

public class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException() {
        super();
    }

    public InsufficientBalanceException(String message) {
        super(message);
    }

    public InsufficientBalanceException(String message, Throwable cause) {
        super(message, cause);
    }
}

Unchecked Exception

繼承 RuntimeException

public class InvalidUserException extends RuntimeException {
    public InvalidUserException() {
        super();
    }

    public InvalidUserException(String message) {
        super(message);
    }

    public InvalidUserException(String message, Throwable cause) {
        super(message, cause);
    }
}

使用自訂例外

public class BankAccount {
    private String accountId;
    private double balance;

    public BankAccount(String accountId, double initialBalance) {
        this.accountId = accountId;
        this.balance = initialBalance;
    }

    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            throw new InsufficientBalanceException(
                "餘額不足,目前餘額:" + balance + ",提款金額:" + amount
            );
        }
        balance -= amount;
    }

    public double getBalance() {
        return balance;
    }
}

// 使用
BankAccount account = new BankAccount("A001", 1000);
try {
    account.withdraw(1500);
} catch (InsufficientBalanceException e) {
    System.out.println(e.getMessage());
    // 餘額不足,目前餘額:1000.0,提款金額:1500.0
}

包含額外資訊

public class InsufficientBalanceException extends Exception {
    private final double currentBalance;
    private final double requestedAmount;

    public InsufficientBalanceException(double currentBalance, double requestedAmount) {
        super(String.format("餘額不足,目前餘額:%.2f,請求金額:%.2f",
            currentBalance, requestedAmount));
        this.currentBalance = currentBalance;
        this.requestedAmount = requestedAmount;
    }

    public double getCurrentBalance() {
        return currentBalance;
    }

    public double getRequestedAmount() {
        return requestedAmount;
    }

    public double getShortfall() {
        return requestedAmount - currentBalance;
    }
}

// 使用
try {
    account.withdraw(1500);
} catch (InsufficientBalanceException e) {
    System.out.println("缺少:" + e.getShortfall());
}

例外層級結構

建立例外的繼承層級:

// 基礎業務例外
public class BusinessException extends Exception {
    private final String errorCode;

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

    public String getErrorCode() {
        return errorCode;
    }
}

// 使用者相關例外
public class UserException extends BusinessException {
    public UserException(String errorCode, String message) {
        super(errorCode, message);
    }
}

// 具體例外
public class UserNotFoundException extends UserException {
    private final String userId;

    public UserNotFoundException(String userId) {
        super("USER_001", "找不到使用者:" + userId);
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }
}

public class DuplicateUserException extends UserException {
    public DuplicateUserException(String email) {
        super("USER_002", "Email 已存在:" + email);
    }
}

使用例外層級

public class UserService {
    public User findById(String id) throws UserNotFoundException {
        User user = repository.findById(id);
        if (user == null) {
            throw new UserNotFoundException(id);
        }
        return user;
    }

    public void register(User user) throws DuplicateUserException {
        if (repository.existsByEmail(user.getEmail())) {
            throw new DuplicateUserException(user.getEmail());
        }
        repository.save(user);
    }
}

// 呼叫端可以選擇處理的精細程度
try {
    userService.findById("123");
} catch (UserNotFoundException e) {
    // 處理找不到使用者
} catch (UserException e) {
    // 處理所有使用者相關例外
} catch (BusinessException e) {
    // 處理所有業務例外
}

錯誤碼設計

public enum ErrorCode {
    USER_NOT_FOUND("U001", "使用者不存在"),
    USER_DUPLICATE("U002", "使用者已存在"),
    INVALID_PASSWORD("U003", "密碼錯誤"),
    ACCOUNT_LOCKED("U004", "帳號已鎖定"),

    ORDER_NOT_FOUND("O001", "訂單不存在"),
    INSUFFICIENT_STOCK("O002", "庫存不足");

    private final String code;
    private final String defaultMessage;

    ErrorCode(String code, String defaultMessage) {
        this.code = code;
        this.defaultMessage = defaultMessage;
    }

    public String getCode() { return code; }
    public String getDefaultMessage() { return defaultMessage; }
}

public class BusinessException extends RuntimeException {
    private final ErrorCode errorCode;

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getDefaultMessage());
        this.errorCode = errorCode;
    }

    public BusinessException(ErrorCode errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }
}

// 使用
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK, "商品 A 庫存不足");

例外包裝

將低層級例外包裝成業務例外:

public class DataAccessException extends RuntimeException {
    public DataAccessException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class UserRepository {
    public User findById(String id) {
        try {
            // 資料庫操作
            return jdbcTemplate.queryForObject(sql, mapper, id);
        } catch (SQLException e) {
            throw new DataAccessException("查詢使用者失敗:" + id, e);
        }
    }
}

實際範例

驗證例外

public class ValidationException extends RuntimeException {
    private final List<String> errors;

    public ValidationException(List<String> errors) {
        super("驗證失敗:" + String.join(", ", errors));
        this.errors = new ArrayList<>(errors);
    }

    public List<String> getErrors() {
        return Collections.unmodifiableList(errors);
    }
}

public class UserValidator {
    public void validate(User user) throws ValidationException {
        List<String> errors = new ArrayList<>();

        if (user.getName() == null || user.getName().isEmpty()) {
            errors.add("名稱不能為空");
        }
        if (user.getEmail() == null || !user.getEmail().contains("@")) {
            errors.add("Email 格式錯誤");
        }
        if (user.getAge() < 0 || user.getAge() > 150) {
            errors.add("年齡必須在 0-150 之間");
        }

        if (!errors.isEmpty()) {
            throw new ValidationException(errors);
        }
    }
}

API 例外

public class ApiException extends RuntimeException {
    private final int statusCode;
    private final String errorCode;

    public ApiException(int statusCode, String errorCode, String message) {
        super(message);
        this.statusCode = statusCode;
        this.errorCode = errorCode;
    }

    public int getStatusCode() { return statusCode; }
    public String getErrorCode() { return errorCode; }
}

public class NotFoundException extends ApiException {
    public NotFoundException(String message) {
        super(404, "NOT_FOUND", message);
    }
}

public class BadRequestException extends ApiException {
    public BadRequestException(String message) {
        super(400, "BAD_REQUEST", message);
    }
}

public class UnauthorizedException extends ApiException {
    public UnauthorizedException() {
        super(401, "UNAUTHORIZED", "未授權存取");
    }
}

Checked vs Unchecked

何時使用 Checked Exception

  • 呼叫者可以合理處理的情況
  • 例如:檔案不存在、網路斷線
public class FileProcessor {
    public void process(String path) throws FileNotFoundException {
        // 呼叫者可以選擇提供其他檔案
    }
}

何時使用 Unchecked Exception

  • 程式邏輯錯誤
  • 呼叫者無法恢復的情況
  • 例如:參數驗證失敗、配置錯誤
public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("年齡不能為負數");
    }
}

最佳實踐

1. 提供多個建構子

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

2. 使例外不可變

public class OrderException extends RuntimeException {
    private final String orderId;      // final
    private final List<String> items;  // 不可變

    public OrderException(String orderId, List<String> items) {
        super("訂單錯誤:" + orderId);
        this.orderId = orderId;
        this.items = List.copyOf(items);  // 防禦性複製
    }
}

3. 適當的序列化

public class CustomException extends Exception {
    private static final long serialVersionUID = 1L;
    // ...
}

重點整理

  • 繼承 Exception 建立 Checked Exception
  • 繼承 RuntimeException 建立 Unchecked Exception
  • 可以加入額外欄位提供更多錯誤資訊
  • 使用例外層級結構組織相關例外
  • 考慮使用錯誤碼便於識別和國際化
  • 使用例外包裝隱藏實作細節
  • 提供完整的建構子集合