Java 异常与错误处理
Java 异常体系、受检/非受检异常、抛出与捕获边界:失败如何成为可设计的控制流。
#type / concept
#status / growing
#tech / dev
#resource / java
[!info] 关联笔记
Java 异常与错误处理
这个概念为什么出现
IO 失败、参数非法、下游超时都是常态。Java 用异常对象 + 栈展开表达失败,并区分受检异常(checked)与非受检异常(unchecked)。不会设计异常边界,就会出现要么“吞异常”,要么“到处 throws Exception”。
[!abstract] 一句话理解 异常是带类型与栈轨迹的失败信号;
Error/RuntimeException非受检,其它Exception子类默认受检;在边界捕获、转译、记录,在内核保持不变量。
最小可运行示例
场景:库存预留——参数错误与“库存不足”要用不同异常策略
class InsufficientStockException extends Exception {
InsufficientStockException(String msg) { super(msg); }
}
public class StockService {
private int stock;
StockService(int stock) { this.stock = stock; }
// reserve:受检异常表达可预期业务失败(示例风格;团队也可选非受检)
// 教学点:非法参数用非受检;业务可恢复失败可用受检或领域异常策略。
int reserve(int qty) throws InsufficientStockException {
if (qty <= 0) {
throw new IllegalArgumentException("qty must be > 0");
}
if (qty > stock) {
throw new InsufficientStockException("need=" + qty + " stock=" + stock);
}
stock -= qty;
return stock;
}
public static void main(String[] args) {
StockService svc = new StockService(5);
try {
System.out.println("left=" + svc.reserve(3));
System.out.println("left=" + svc.reserve(10));
} catch (InsufficientStockException ex) {
System.out.println("biz-fail: " + ex.getMessage());
} catch (IllegalArgumentException ex) {
System.out.println("bad-arg: " + ex.getMessage());
}
}
}
建议运行:
javac StockService.java && java StockService
期望输出:
left=2
biz-fail: need=10 stock=2
结合场景再看三个关注点
- 异常类型本身就是 API,调用方靠类型分支处理。
- 不要 catch Exception 后空着——至少记录或转译。
- 受检异常强迫声明;滥用会污染签名,需要分层转译。
核心概念与准确模型
1. 层次(简化)
Throwable
├── Error // 严重,通常不捕获恢复
└── Exception
├── RuntimeException // 非受检
└── 其它 Exception // 受检
2. 关键词
throw抛出throws声明try/catch/finally捕获与清理(资源优先 try-with-resources)
3. 设计问题
| 问题 | 建议 |
|---|---|
| 编程错误(NPE、越界) | 非受检,修代码 |
| 可预期外部失败 | 受检或明确领域非受检 + 文档 |
| 跨层 | 转译为调用方能理解的类型 |
| 日志 | 在边界记一次,避免每层重复刷屏 |
4. 异常链
throw new IllegalStateException("pay failed", cause);
保留 cause 便于排障。
边界情况与反直觉行为
finally中 return 会掩盖 try 中异常/返回值——禁止。- 捕获后重新抛要保留栈或正确包装。
- 异步线程中的异常不会自动回到提交者——见线程池。
常见误区
[!warning] 常见误区:
catch (Exception e) {}静默吞掉会把故障变成数据错误,极难排障。
[!warning] 常见误区:用异常做正常分支导流 成功路径高频场景用返回值/Optional;异常给“失败路径”。
工程实践
- 公共模块定义稳定异常类型。
- 控制器/入口层统一映射 HTTP/错误码。
- 日志含业务关键 id,避免只打堆栈。
- 测试既要测成功也要测异常契约。
本节总结
- 异常是类型化失败控制流
- 受检/非受检是 API 设计选择
- 边界处理 + 原因链 + 不吞异常
自测题
Error为什么通常不在业务里 catch 恢复?- 何时包装异常而不是原样抛出?
参考答案
- 多表示环境/JVM 级严重问题,业务恢复策略通常不成立。
- 跨越抽象边界时转译为调用方语义,同时保留 cause。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Exceptions (Oracle Tutorial) | 教程 | 异常模型 |
| JLS Exceptions | 规范 | 异常语义 |