理解为何需要自定义异常,以及它在业务系统中的价值
掌握创建检查型与非检查型自定义异常的方法
能够根据业务场景选择合适的异常类型
实现清晰、可维护的异常处理结构,分离业务逻辑与错误处理
避免使用模糊错误信息,提升程序可读性与调试效率
在实际开发中,标准异常(如 IllegalArgumentException)往往无法准确表达业务语义。例如:
public class PoorExample {
public static void main(String[] args) {
int age = 15;
if (age < 18) {
System.out.println("Error"); // 模糊、无上下文、难调试
}
}
}上述做法的问题:
错误信息不明确,无法快速定位问题
没有抛出异常对象,调用方无法捕获或处理
业务逻辑与错误提示混杂,违反单一职责原则
在大型系统中难以追踪和监控特定业务错误
解决方案:使用自定义异常,将业务规则错误封装为有意义的异常类型。
在 Java 中,自定义异常通过继承 Exception 或 RuntimeException 实现。
通用模板
// 检查型自定义异常
class MyCheckedException extends Exception {
public MyCheckedException(String message) {
super(message);
}
}
// 非检查型自定义异常
class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String message) {
super(message);
}
}可选增强:
添加错误码字段(如
errorCode)提供无参构造器
支持嵌套异常(
Throwable cause)
检查型自定义异常
适用于可恢复的业务错误,如用户输入不符合规则、外部资源不可用等。
场景:年龄验证(注册限制)
// 自定义检查型异常
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
public class AgeValidator {
// 方法声明可能抛出检查型异常
public static void validateAge(int age) throws InvalidAgeException {
if (age < 18) {
throw new InvalidAgeException("用户年龄必须满 18 岁才能注册");
}
System.out.println("年龄验证通过: " + age + " 岁");
}
public static void main(String[] args) {
try {
validateAge(16);
} catch (InvalidAgeException e) {
System.err.println("注册失败: " + e.getMessage());
}
}
}注册失败: 用户年龄必须满 18 岁才能注册优势:
编译器强制调用者处理该异常(
try-catch或throws)明确表达了“这是一个需要处理的业务规则”
便于上层做不同策略(如提示用户、记录日志、返回错误码)
非检查型自定义异常
适用于编程错误或不可恢复的逻辑错误,如非法状态、参数校验失败等。
场景:除零保护
// 自定义非检查型异常
class DivisionByZeroException extends RuntimeException {
public DivisionByZeroException(String message) {
super(message);
}
}
public class Calculator {
public static int safeDivide(int dividend, int divisor) {
if (divisor == 0) {
throw new DivisionByZeroException("除数不能为零");
}
return dividend / divisor;
}
public static void main(String[] args) {
try {
int result = safeDivide(10, 0);
System.out.println("结果: " + result);
} catch (DivisionByZeroException e) {
System.err.println("计算错误: " + e.getMessage());
}
}
}计算错误: 除数不能为零注意:
编译器不要求处理此异常(但建议捕获以避免程序崩溃)
更适合用于“本不该发生的错误”,属于防御性编程的一部分
如何选择
用检查型异常:当调用者有能力且应该处理该错误时(如重试、提示用户、切换备用方案)。
用非检查型异常:当错误表示程序缺陷,不应被忽略,且通常无法合理恢复时。
便于国际化或多端响应
class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
// 使用
throw new BusinessException("USER_AGE_INVALID", "年龄不符合注册要求");保留原始上下文
class DataAccessException extends Exception {
public DataAccessException(String message, Throwable cause) {
super(message, cause);
}
}
// 使用
try {
// 数据库操作
} catch (SQLException e) {
throw new DataAccessException("数据库查询失败", e);
}命名规范:异常类名以 Exception 结尾(如 InsufficientBalanceException)。
语义清晰:异常名称应准确反映业务含义,避免泛化(如 MyException)。
避免过度设计:不是每个错误都需要自定义异常,优先使用标准异常。
文档化:在方法注释中说明可能抛出的自定义异常及其触发条件。
统一异常体系:在项目中建立基类异常(如 BaseBusinessException),便于全局处理。
自定义异常让业务错误显式化、结构化、可追踪
检查型异常适用于可恢复的外部错误,非检查型适用于内部逻辑错误
通过继承 Exception 或 RuntimeException 创建自定义异常
合理使用自定义异常可显著提升代码的健壮性、可读性和可维护性
现代应用常结合全局异常处理器(如 Spring 的 @ControllerAdvice)统一响应格式
在一个电商系统中,“库存不足”应该设计为检查型还是非检查型异常?为什么?
如果多个自定义异常具有相似结构(如都包含错误码),如何避免重复代码?
能否在自定义异常中添加业务数据(如用户 ID、订单号)?这样做是否合理?