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 - 可以加入額外欄位提供更多錯誤資訊
- 使用例外層級結構組織相關例外
- 考慮使用錯誤碼便於識別和國際化
- 使用例外包裝隱藏實作細節
- 提供完整的建構子集合