bigywater.tech
Java 内部原理 · 29

try-with-resources 真实是逆序关闭:被抑制的异常真实附在主异常身上,不是丢了

系列前面验证过 finally 里的 return 会真实吞掉异常。try-with-resources 的隐式 close() 调用面对同样的场景,处理方式完全不一样——真实验证它到底是怎么做的。

C → B → A
真实验证:三个资源按 A、B、C 声明顺序打开,关闭真实是 C、B、A,和声明顺序正好相反
0 个异常真的丢失
try 主体和两个资源的 close() 同时真实抛异常,真实通过 getSuppressed() 拿到全部 2 个被抑制的异常,一个都没丢

上一篇:泛型可变参数的堆污染:真实复现一次编译器拦不住的 ClassCastException

真实验证:关闭顺序和声明顺序正好相反

try-with-resources 括号里声明的多个资源,关闭时应该按什么顺序——真实运行观察打开和关闭的日志顺序。

TryWithResourcesDemo.java try (Res a = new Res("A", false); Res b = new Res("B", false); Res c = new Res("C", false)) { System.out.println("try 块内部执行中..."); }
真实实测
打开 A
打开 B
打开 C
try 块内部执行中...
关闭 C
关闭 B
关闭 A

真实结果是逆序关闭(先声明的最后关):C 先关、然后 B、最后 A。这个顺序符合直觉——后声明的资源往往依赖先声明的资源(比如先开文件再包一层缓冲流),关闭时理应先关"外层"的、再关"内层"的,这样任何一个资源的 close() 内部逻辑用到更早声明的资源时,那个资源还真实处于可用状态。

真实验证:被抑制的异常,真的没有丢

如果 try 主体抛了异常,两个资源的 close() 也各自真实抛了异常,调用方最终能看到几个异常、分别是什么?

TryWithResourcesDemo.java try (Res a = new Res("A", true); Res b = new Res("B", true)) { throw new IllegalStateException("try 块主体抛出的异常"); } catch (Exception e) { Throwable[] suppressed = e.getSuppressed(); // ... }
真实实测
打开 A
打开 B
关闭 B
关闭 A

catch 到的主异常 = try 块主体抛出的异常
真实附带的 suppressed 异常个数 = 2
  suppressed: B 关闭时抛出的异常
  suppressed: A 关闭时抛出的异常

真实结果和 finally 里裸写 return/throw 完全不同:调用方 catch 到的是 try 主体抛出的那个"最原始"的异常(通常也是最有诊断价值的那个,因为它发生在问题的源头),两个 close() 真实抛出的异常都没有丢失,而是通过 e.getSuppressed() 真实挂在了主异常身上——顺序是先 B 后 A,正好是真实的关闭顺序。

为什么这次没有像 finally 里的 return 那样"覆盖"?这是 try-with-resources 在 Java 7 引入时专门设计的行为(由编译器生成的 addSuppressed 调用实现),目的就是修复手写 try/finally 手动调 close() 时最常见的那个问题——手写代码里,finally 块里调用 close() 抛出的异常会真实覆盖掉 try 块里原本的异常(和系列上一篇验证的 finally 里裸写 return/throw 是同一个根源问题),原始的、往往更有诊断价值的异常反而丢了。try-with-resources 从语言层面解决了这个问题,不需要开发者自己动手维护 suppressed 逻辑。

分步演示:关闭顺序,和异常是怎么被真实"追加"而不是覆盖的

每一步都是真实运行这段代码时打印出的顺序,不是按规范描述手工排的。

A、B、C 逆序关闭(C→B→A);两个 close() 都抛异常,主异常和 2 个 suppressed 异常一个都没丢
这一步刚记录的日志

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到。
    • 和finally 里的 return 那篇是同一主题的正反两面,建议对照读:手写 finally 会真实丢异常,try-with-resources 从语言层面真实解决了这个问题。
    • 没有涉及:资源实现 AutoCloseable 时 close() 抛检查异常和抛运行时异常在这套机制下有没有区别(结论是没有区别,suppressed 机制对两者一视同仁)——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号