1. 异常继承体系的核心设计逻辑
Java异常处理机制的本质是对程序非正常状态的分类管理。所有异常都继承自Throwable类,其下分为Error和Exception两大分支。Error表示JVM层面的严重问题(如OutOfMemoryError),而Exception才是我们业务开发中需要处理的异常类型。
在Exception体系中,RuntimeException及其子类属于非受检异常(Unchecked Exception),其他Exception子类则是受检异常(Checked Exception)。这种区分不是随意的,而是基于异常的可恢复性设计的:
- 受检异常:调用方必须处理的预期异常(如IOException)
- 非受检异常:通常表示编程错误(如NullPointerException)
关键设计原则:如果调用者能够合理地预期某种异常并采取恢复措施,就使用Checked Exception;如果异常表示程序错误(如参数校验失败),则使用RuntimeException。
2. 自定义异常的典型应用场景
2.1 业务规则校验异常
电商系统中的库存不足异常(InsufficientStockException)应该继承RuntimeException。因为:
- 调用方通常无法从这种错误中恢复
- 这是参数校验的延伸(相当于前置校验没做好)
- 强制捕获处理会增加无意义的代码复杂度
java复制public class InsufficientStockException extends RuntimeException {
private final String sku;
private final int requested;
private final int available;
// 构造器包含完整的错误上下文信息
public InsufficientStockException(String sku, int requested, int available) {
super(String.format("SKU %s 库存不足 (请求%d件,实际%d件)", sku, requested, available));
this.sku = sku;
this.requested = requested;
this.available = available;
}
}
2.2 外部服务调用异常
支付网关超时异常(PaymentGatewayTimeoutException)则应该继承Exception:
- 网络问题是可以预期的
- 调用方可能需要重试或降级处理
- 属于业务正常流程的一部分
java复制public class PaymentGatewayTimeoutException extends Exception {
private final String gatewayUrl;
private final Duration timeout;
public PaymentGatewayTimeoutException(String gatewayUrl, Duration timeout) {
super(String.format("支付网关 %s 响应超时 (%d秒)", gatewayUrl, timeout.getSeconds()));
this.gatewayUrl = gatewayUrl;
this.timeout = timeout;
}
}
3. 面试中的高频陷阱题解析
3.1 陷阱题示例
java复制public class CustomException extends Exception {
// 空的实现
}
// 问题:以下代码有什么问题?
try {
throw new CustomException();
} catch (RuntimeException e) {
System.out.println("捕获RuntimeException");
}
考点分析:
- 异常捕获是基于类型匹配的
- CustomException继承链:Throwable <- Exception <- CustomException
- 与RuntimeException没有继承关系
- 正确做法是捕获CustomException或更上层的Exception
3.2 进阶陷阱题
java复制class BaseException extends Exception {}
class SubException extends BaseException {}
public void process() throws BaseException {
throw new SubException();
}
// 调用方代码:
try {
process();
} catch (SubException e) { // 编译错误
// ...
}
问题本质:
Java的异常声明遵循"里氏替换原则":
- 方法可以抛出声明异常的子类
- 但catch块必须精确匹配或使用父类
4. 自定义异常的最佳实践
4.1 构造器设计要点
- 必须提供包含详细信息的构造器
- 建议实现以下四种标准构造器:
java复制public MyException()
public MyException(String message)
public MyException(String message, Throwable cause)
public MyException(Throwable cause)
4.2 异常链的正确使用
java复制try {
// 业务代码
} catch (IOException e) {
throw new BusinessException("处理业务时发生IO错误", e); // 保留原始异常
}
4.3 性能优化建议
- 重写fillInStackTrace()方法对于高频抛出的异常:
java复制@Override
public synchronized Throwable fillInStackTrace() {
return this; // 避免昂贵的堆栈跟踪
}
- 异常实例应该是不可变的(final字段)
- 考虑使用预创建的异常实例(对于常见错误)
5. 项目实战中的典型错误案例
5.1 过度使用Checked Exception
反例:用户登录时密码错误使用Checked Exception
java复制public void login(String username, String password) throws InvalidCredentialException {
if (!checkPassword(username, password)) {
throw new InvalidCredentialException("密码错误");
}
}
问题在于:
- 密码错误是正常业务流程的一部分
- 强制处理会增加调用方负担
- 更适合用返回值或Unchecked Exception
5.2 异常信息不完整
反例:
java复制throw new DatabaseException("数据库错误");
改进后:
java复制throw new DatabaseException(
"执行SQL失败 [SQL:" + sql + "] [Params:" + params + "]",
e
);
5.3 异常吞噬问题
错误做法:
java复制try {
riskyOperation();
} catch (Exception e) {
// 什么都没做!
}
至少应该:
- 记录日志
- 转换为适合上层处理的异常
- 必要时调用Thread.currentThread().interrupt()
6. 面试应答策略与话术
当面试官问"什么时候该用Checked Exception"时,可以这样回答:
"这取决于异常的性质和调用方的处理能力。我的判断标准是:
- 如果调用方能够采取有意义的恢复措施 → Checked
- 如果错误表明程序逻辑有问题 → Unchecked
- 如果是业务流程的正常分支 → 考虑用返回值代替异常
比如在支付系统中:
- 网络超时是Checked(可重试)
- 金额为负是Unchecked(参数校验问题)
- 余额不足可能是返回值(业务正常状态)"
这种回答展示了:
- 对异常分类的深刻理解
- 结合实际场景的思考
- 考虑问题全面性的能力
