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