Java 中 try-with-resources 如何正确处理多个资源的关闭顺序?
try-with-resources 中多个资源按声明逆序关闭,有依赖时应先声明被依赖资源,使包装类先关闭、底层资源后关闭;close() 抛出的异常会作为 suppressed 挂到主异常上。
学完这篇,你能准确说出 try-with-resources 中多个资源的关闭顺序,并写出资源有依赖、close() 又抛异常时不会踩坑的代码。
第一步:把资源写进 try 的括号里,确认类型实现了 AutoCloseable
这一步的目标是让编译器接管关闭动作,你不需要写 finally。资源类必须实现 java.lang.AutoCloseable(Closeable 是它的子接口),否则括号里写不进去,编译直接报错。
先写一个能观察顺序的测试类,存成 CloseOrderDemo.java:
public class CloseOrderDemo {
static class Res implements AutoCloseable {
private final String name;
Res(String name) { this.name = name; System.out.println("open " + name); }
@Override public void close() { System.out.println("close " + name); }
}
public static void main(String[] args) {
try (Res a = new Res("A");
Res b = new Res("B");
Res c = new Res("C")) {
System.out.println("body");
}
}
}
多个资源之间用分号分隔,最后一个可以不加分号。
注意:
javac默认编码在 Windows 中文环境下可能不是 UTF-8,含中文注释时加-encoding UTF-8,否则可能报「编码 GBK 的不可映射字符」。
第二步:记住「后声明先关闭」的规则并编译验证
这一步要拿到实际输出,确认关闭顺序与声明顺序相反。执行:
javac -encoding UTF-8 CloseOrderDemo.java
java CloseOrderDemo
输出为:
open A
open B
open C
body
close C
close B
close A
结论:打开顺序是声明顺序,关闭顺序严格反过来,也就是最后声明的 c 最先关闭,最先声明的 a 最后关闭。
第三步:按依赖关系排列声明顺序
这一步要解决实际问题:当一个资源依赖另一个资源时,声明顺序决定谁先关。规则是「被依赖的写在前面,依赖别人的写在后面」,这样关闭时包装类先关、底层流后关。
try (FileReader fr = new FileReader("data.txt");
BufferedReader br = new BufferedReader(fr)) {
System.out.println(br.readLine());
}
这里 br 先关闭(会把缓冲刷出去),fr 后关闭。如果反过来写,语法上就通不过,因为构造 br 时 fr 还不存在。
注意:涉及锁、数据库连接池、事务这类资源时,同样遵循这个顺序——先拿到的锁最后释放。顺序写反不会编译报错,但运行时可能出现「在已关闭的连接上操作」这类异常。
第四步:处理 close() 自己抛异常的情况
这一步要搞清楚异常优先级。close() 也可能失败,Java 的处理方式是:**try 块里的异常作为主异常抛出,关闭时产生的异常被挂到主异常的 suppressed 列表里**,用 Throwable.getSuppressed() 取。
try (Res a = new Res("A")) {
throw new RuntimeException("body 出错");
} catch (Exception e) {
System.out.println("主异常: " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println("被抑制: " + s.getMessage());
}
}
如果 try 块本身没抛异常,那么最先声明的资源(最后关闭的那个)抛出的异常会成为主异常,其余关闭异常被抑制。所以排查时不要只看第一条堆栈,printStackTrace() 会自动打印 Suppressed: 段落,务必往下翻。
注意:不要用
catch (Exception e) { }把异常吞掉,那样连 suppressed 信息一起消失,线上排查会变得非常困难。
第五步:Java 9 及以上改用 effectively final 变量
这一步是语法简化。如果资源在 try 之前就要创建(比如需要参与其他逻辑),JDK 9 起可以直接在括号里写变量名:
FileReader fr = new FileReader("data.txt");
BufferedReader br = new BufferedReader(fr);
try (fr; br) {
System.out.println(br.readLine());
}
要求变量是 final 或「effectively final」(声明后不再被重新赋值)。顺序规则完全不变,仍然是 br 先关、fr 后关。
注意:这个写法在 JDK 8 上会编译失败(
unexpected token)。团队项目里先跑java -version确认版本,别在 8 的环境里照抄。另外,如果变量值为null,try-with-resources 会跳过关闭、不抛 NPE,但别依赖这个行为掩盖初始化失败。
小结
- 多资源用分号分隔写在同一对括号里,打开按声明顺序,关闭严格逆序。
- 有依赖关系时,被依赖的资源写在前面,包装类写在后面。
close()抛出的异常不会中断其余资源的关闭,它们被压进 suppressed 列表,用getSuppressed()查看。- try 块无异常时,主异常来自最先声明的资源(最后关闭的那个)。
- JDK 9+ 可用
try (fr; br)引用 effectively final 变量,JDK 8 不支持。 - 别用空 catch 吞异常,否则会丢失 suppressed 信息和真实关闭原因。
转载请注明出处,版权归原作者所有。