1785 字
约 5 分钟
4
S04-J09 · 异常体系:受检与否、`finally` 的四种失效与资源关闭

适用读者:写业务代码总被"异常怎么处理"卡住的人;要答"为什么 Java 要受检异常"的人。
前置:S04-J07、J08(Error/Exception 都在 java.lang)。
覆盖旧文:2744–2748(异常基础、throw/throws、自定义异常、try)+ 2988–2992(同题重复)+ 4428(遇到的错误总结)→ 12 篇 / 约 8 万字符。
扩展点:E-S04-027(Throwable 三层结构与何时捕获)、E-S04-028(finally 失效的四种情形)、E-S04-029(try-with-resources 与 suppressed 异常顺序)。
下一篇:S04-J10 · 集合与 IO 补编。


1. 一句话结论

Java 异常的唯一设计原则是"调用方能否做点什么"——能处理的就是受检异常(必须声明),不能处理的(程序 bug、环境崩溃)就是运行时异常或 Error(不必声明)。判断标准不是"严重程度",而是"可恢复性"。

出处:A-2744–A-2748(异常四章)、A-2988–A-2992、A-4428(错误总结)。


2. Throwable 三层结构(E-S04-027)

Throwable
├── Error            不要捕获(OutOfMemoryError / StackOverflowError / NoClassDefFoundError)
└── Exception
    ├── RuntimeException(非受检)  编程错误:NPE / IndexOutOfBounds / IllegalArgument / ClassCast
    └── 其他(受检)               外部条件:IOException / SQLException / InterruptedException
类型 是否必须声明 典型来源 处理策略
Error 否 JVM 资源耗尽、链接失败 记录 + 快速失败(多数无法恢复)
RuntimeException 否 代码缺陷 修 bug,不要用 catch 掩盖
受检 Exception ✅ IO、网络、数据库、并发中断 重试 / 降级 / 包装后上抛

判据:catch (Exception e) {} 空实现是最高频的技术债——它把"失败"变成"静默错数据"。

2.1 throw 与 throws

public void read(String p) throws IOException {   // throws:声明可能抛出(受检必须声明)
    if (p == null) throw new IllegalArgumentException("path is null");  // throw:抛出一个实例
}

边界:throws 只对受检异常强制;子类重写方法不能抛出比父类更宽的受检异常(可以更窄,也可以不抛)。

2.2 自定义异常:两条规约

public class BizException extends RuntimeException {   // 业务异常通常用非受检
    private final String code;
    public BizException(String code, String msg) { super(msg); this.code = code; }
    public BizException(String code, String msg, Throwable cause) { super(msg, cause); this.code = code; }
}
规约 原因
保留 cause 链 丢 cause 会切断栈,排障时看不到根因
业务异常继承 RuntimeException 避免污染每个方法签名;由统一异常处理器兜底

3. finally 失效的四种情形(E-S04-028)

try { ... } finally { ... }
情形 finally 是否执行 说明
正常返回 / 抛异常 ✅ 常规路径
finally 里有 return ✅ 执行,但覆盖 try 的返回值/异常 最危险的写法
System.exit(0) / JVM 崩溃 ❌ 进程终止
守护线程被强制结束 / Runtime.halt ❌ 非正常终止
static int f() {
    try { return 1; } finally { return 2; }   // 返回 2,try 的 1 被丢弃
}
static int g() {
    int x = 1;
    try { return x; } finally { x = 2; }      // 返回 1(返回值已入栈)
}

坑 1:finally 里 return/throw 会吞掉 try 中的异常——线上表现为"日志里没有异常,但结果错了"。


4. try-with-resources(E-S04-029)

try (var in = new FileInputStream("a.txt");
     var out = new FileOutputStream("b.txt")) {     // Java 9+ 可省略类型
    in.transferTo(out);
}   // 逆序自动 close,close 抛出的异常进 suppressed
问题 答
关闭顺序 逆序(后声明的先关)
主异常 + close 异常并存 主异常抛出,close 的异常挂在 getSuppressed()
资源类要求 必须实现 AutoCloseable(Closeable 是其子接口,抛 IOException)
close() 里再抛异常 不会覆盖主异常(与 finally 的手写 close 相反)

判据:"手写 finally close"的代码几乎都能换成 try-with-resources——前者会把主异常替换成 close 异常,这是排障时"根因丢失"的常见来源。


5. 异常处理的四条工程规约

规约 反例 后果
不要空 catch catch (Exception e) {} 失败静默,数据错乱
不要用异常控流程 try { ... } catch (NullPointerException e) { } 栈开销 + 掩盖真实缺陷
包装要带 cause throw new BizException("失败") 栈断裂
分层处理 Controller 层统一兜底、Service 层转换、DAO 层透传 各层职责清晰
// 统一兜底(Spring 风格示意)
@RestControllerAdvice
class GlobalHandler {
    @ExceptionHandler(BizException.class)
    ResponseEntity<?> biz(BizException e) { return ResponseEntity.badRequest().body(e.getCode()); }
    @ExceptionHandler(Exception.class)
    ResponseEntity<?> other(Exception e) { log.error("unexpected", e); return ResponseEntity.status(500).build(); }
}

6. 常见异常速查(来自 A-4428 错误总结)

异常 真实原因 处理
NullPointerException 空引用调用/自动拆箱 Integer 为 null 提前判空、Objects.requireNonNull、Optional
ClassCastException 泛型擦除后强转 用 instanceof 先判断(见 J08)
ConcurrentModificationException 遍历中修改集合 removeIf / 迭代器 remove(见 J01)
NumberFormatException 字符串转数字非法 校验 + tryParse 封装
InterruptedException 线程被中断 捕获后必须 Thread.currentThread().interrupt() 恢复标志位
OutOfMemoryError: Metaspace 类加载过多(热部署/动态代理) 见 S25-J08 四类 OOM
NoClassDefFoundError vs ClassNotFoundException 前者类初始化失败/运行期缺类;后者编译期存在、运行期找不到 查依赖冲突与类加载器

7. 反例:五种"看起来更安全"的错法

写法 为什么错
catch (Exception e) { log(e); } 后继续执行 上层拿到"成功"但数据没写
finally { return; } 吞异常、覆盖返回值
手写 finally { close(); } close 异常覆盖主异常
catch (InterruptedException e) { } 丢失中断标志,线程池无法优雅停机
把受检异常全部转成 RuntimeException 丢失"调用方应处理"的契约

8. 自测

  1. 受检与非受检的划分标准是什么?Error 为何不捕获?
  2. finally 在哪些情况下不执行?finally 里 return 会怎样?
  3. try-with-resources 的关闭顺序?主异常与 close 异常如何共存?
  4. 捕获 InterruptedException 后必须做什么?
  5. NoClassDefFoundError 与 ClassNotFoundException 的区别?
答案
  1. 调用方是否能恢复处理;Error 多为 JVM 级不可恢复故障。
  2. System.exit/JVM 崩溃/守护线程强制结束;return 覆盖返回值并吞异常。
  3. 逆序关闭;主异常抛出,close 异常进 getSuppressed()。
  4. 恢复中断标志 Thread.currentThread().interrupt()。
  5. 前者是运行期类初始化/加载失败(曾编译通过),后者是显式按名加载找不到类。

9. 下一篇

S04-J10 · 集合与 IO 补编 —— Vector/Stack 为何废弃、Queue 两套语义、List/Set/Map 接口差异、字节流与字符流的桥接、Files 工具类与四种 IO 模型。


本篇属于系列《S04 · Java 语言核心》。旧文来源:A-2744–2748、A-2988–2992、A-4428;扩展点 E-S04-027/028/029。对照实验数字为机制推断,未经 JMH 实测。

S04-J09 · 异常体系:受检与否、`finally` 的四种失效与资源关闭
https://www.clxhxhhr.top/posts/5232/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。