绝大多数业务代码中不该直接抛出或层层throws checked exception,因其强制处理常导致空catch、异常吞没或暴露实现细节;spring等框架通过异常翻译转为runtimeexception并统一处理,兼顾语义清晰与接口简洁。

Checked Exception到底该不该用?先说结论
绝大多数业务代码里,Checked Exception 不该直接抛出,更不该层层 throws。它在设计上想强制你处理错误,结果常逼你写一堆空 catch、无效包装或吞掉异常——这比不处理还糟。
为什么 IOException 这类异常让人头疼?
不是它本身有问题,而是它出现在错误的地方。比如一个服务层方法签名写着 public User getUser() throws IOException,调用方立刻被拖进技术细节:你根本不需要知道底层是读文件还是查缓存,但编译器硬要你声明或捕获。
- 暴露实现细节:上层被迫感知
SQLException、IOException,违反分层原则 - 污染接口契约:一个本该表达“用户不存在”的业务语义,却被
throws FileNotFoundException带偏 - 催生反模式:为过编译,写出
catch (IOException e) { e.printStackTrace(); },日志没进 ELK,告警也没触发
Spring 和 Hibernate 是怎么绕开它的?
它们没废除 Checked Exception,而是做了「异常翻译」:把底层的 SQLException 包装成 DataAccessException(继承 RuntimeException),再由统一异常处理器(@ControllerAdvice)兜底转换成 HTTP 状态码和 JSON 错误体。
- 业务方法签名干净:
public Order createOrder(OrderRequest req),不带throws - 异常有明确语义:
DuplicateOrderException比SQLIntegrityConstraintViolationException更易懂 - 统一响应格式:所有异常最终走同一个出口,避免
try-catch散落在各处
示例:你不用在每个 service 方法里写 try-catch,而是在全局处理器里这样收口:
立即学习“Java免费学习笔记(深入)”;
if (e instanceof DuplicateKeyException) {
return ResponseEntity.status(409).body("订单号已存在");
}
函数式编程里怎么处理 Checked Exception?
Stream.map()、CompletableFuture.supplyAsync() 这些 API 的函数式接口(如 Function、Supplier)不声明抛异常,所以你不能直接在 lambda 里 throw IOException —— 编译直接报错。
- 常见错误写法:
list.stream().map(s -> Files.readString(Path.of(s)))→ 编译失败 - 正确做法:封装一层工具方法,把
IOException转成RuntimeException抛出 - 或者用
Optional封装结果:Optional<string> content = safeReadString(path)</string>,让调用方决定是否处理空值
关键点在于:不要为了迁就 lambda 去 catch 吞异常,也不要硬改接口加 throws —— 那等于把问题推给下一层。
真正难的不是语法怎么写,而是判断哪个异常值得让调用方「必须处理」。比如 InterruptedException 就是少数仍保留为 Checked Exception 的合理案例,因为线程中断是协作式语义,忽略它可能导致资源泄漏或逻辑卡死——这种例外,恰恰说明规则不是用来打破的,而是用来掂量的。










