异常设计回答三个问题:谁能处理、需要保留哪些上下文、资源如何收尾。把所有异常捕获后返回 null,只是把一个明确
失败变成更难定位的后续错误。
Table of contents
建立失败分类
**受检异常(Checked Exception)**必须在方法签名声明或被捕获,例如 IOException。**非受检异常(Unchecked
Exception)**继承 RuntimeException,编译器不强制处理。
分类不要只看继承关系,还要看恢复责任:
| 失败 | 示例 | 合适的处理者 |
|---|---|---|
| 业务拒绝 | 余额不足、库存不足 | 接口层转换成明确结果 |
| 输入或编程错误 | 参数为 null、状态不可能 | 尽早失败并修复代码 |
| 短暂基础设施故障 | 超时、连接重置 | 有幂等保障的调用层重试 |
| 永久基础设施故障 | 文件不存在、权限拒绝 | 终止操作并保留原因 |
不要用异常表示正常分支,例如“列表没有元素”。也不要仅凭“受检还是非受检”决定是否重试。
运行资源关闭示例
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class 文件首行读取器 {
static String 读取(Path path) {
try (var reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line = reader.readLine();
if (line == null) throw new IllegalArgumentException("文件为空: " + path);
return line;
} catch (IOException e) {
throw new 读取失败异常("读取文件失败: " + path.toAbsolutePath(), e);
}
}
static final class 读取失败异常 extends RuntimeException {
读取失败异常(String message, Throwable cause) { super(message, cause); }
}
}
try-with-resources 会在代码块结束时调用 AutoCloseable.close(),即使读取中途抛出异常也会关闭资源。若业务操作和
关闭动作都失败,前者是主异常,关闭异常会出现在 getSuppressed() 中,不会悄悄丢失。
保留异常链
转换异常时必须把原异常作为 cause 传入。错误消息加入当前层掌握的上下文,例如文件绝对路径或订单号,但不要加入
密码、Token、完整身份证号等敏感数据。
throw new 读取失败异常("读取文件失败: " + path.toAbsolutePath(), e);
以下写法会破坏根因:
catch (IOException e) {
throw new RuntimeException("失败"); // 丢失原异常和操作对象
}
重试前先证明幂等
**幂等(Idempotency)**是同一操作执行一次和多次产生相同业务结果。读取通常可重试;“扣款后连接断开”不能直接重试, 因为第一次可能已经成功。重试策略至少要限制次数、设置退避,并只覆盖明确的短暂错误。
下一步
阅读集合框架的选择与复杂度,学习用数据结构表达语义。