java异常沿调用栈向上冒泡,未捕获则jvm打印堆栈并终止线程;传播路径严格逆序回溯,无智能路由;受检异常须声明throws,运行时异常可省略;finally用于清理而非恢复,再抛异常会覆盖原始异常;包装异常必须保留cause链以保障调试信息。

异常不被捕获时,到底往哪跑?
Java 的异常默认会沿着方法调用栈向上“冒泡”,直到被 try-catch 捕获,或到达 main() 方法仍未处理——此时 JVM 打印堆栈并终止线程。
这不是“自动寻找 handler”,而是严格按调用链逆序查找:谁调了你,就传给谁;谁调了谁,就继续往上。没有“就近捕获”这种智能路由,只有机械回溯。
- 如果
methodC()抛出IOException,而methodB()只声明throws SQLException却没 catch,那异常直接穿透methodB()交给methodA() -
RuntimeException子类(如NullPointerException)可以跳过throws声明,但传播路径完全一样——只是编译器不管它 - 一旦某个方法用
catch吞掉异常又没重新抛出,传播就彻底中断;后续代码看不到这个异常,也收不到任何通知
为什么 throws 声明和实际抛出必须对得上?
这是编译期强制契约:throws 列出的,是该方法**可能向上抛出**的受检异常(Exception 及其子类,但排除 RuntimeException)。不对齐会导致编译失败,不是运行时问题。
- 写了
throws IOException,但方法体里只抛IllegalArgumentException?编译通过——因为后者是运行时异常,不强制声明 - 写了
throws IOException,却在内部throw new SQLException()且没 catch?编译报错:“unreported exception SQLException” - 子类重写父类方法时,
throws只能缩小范围(如父类声明Exception,子类可改写为IOException),不能扩大——否则破坏多态安全性
finally 一定会执行,但别指望它“兜底”异常
finally 块的核心价值是资源清理,不是错误恢复。它的执行优先级很高,但无法改变异常传播结果。
立即学习“Java免费学习笔记(深入)”;
- 如果
try中抛出异常,catch没处理而是直接return,finally仍会执行,但返回值已被锁定 -
finally里再抛新异常,会覆盖try中的原始异常——原始堆栈信息丢失,这是严重隐患 - 不要在
finally里写业务逻辑判断,比如“如果异常是 X 就重试”——它本不该承担这个职责
异常链(cause)不是可选项,是调试刚需
当你要包装异常(比如把底层 SQLException 转成业务层 UserNotFoundException),必须用带 cause 的构造函数,否则原始错误上下文永久丢失。
throw new UserNotFoundException("User 123 not found", sqlEx); // ✅ 保留 cause
否则,日志里只剩顶层异常,排查时根本不知道数据库连接在哪断的。
- 用
e.getCause()或e.printStackTrace()才能看到完整链路 - Lombok 的
@SneakyThrows会绕过编译检查,但不会帮你维护异常链——手动包装时仍需显式传cause - Spring 等框架的全局异常处理器(
@ControllerAdvice)依赖异常链还原原始错误,链断了就只能猜
Throwable 里——所以别轻易 new Exception("xxx"),更别用字符串拼接掩盖真实原因。








