本节目标:理解
AutoCloseable的契约,能正确使用 try-with-resources 管理多个资源,说清逆序关闭和异常抑制的规则,并把审计日志与借阅报表接到library上。
适用版本:JDK 25 LTS(Temurin 25.0.5);try-with-resources 与getSuppressed()在 JDK 21 上行为一致。
上一节留下的问题
library 的借阅操作需要写审计日志,导出报表需要写字符流。只要涉及「打开—使用—关闭」,就要保证关闭一定执行。最朴素的写法是 try/finally:
AuditLog log = new AuditLog(sink, "borrow");
try {
log.log("borrow 978-7-111-54742-6");
} finally {
log.close();
}
一个资源还好。两个资源就要嵌套:
AuditLog outer = new AuditLog(sink, "outer");
try {
AuditLog inner = new AuditLog(sink, "inner");
try {
outer.log("borrow");
inner.log("write");
} finally {
inner.close();
}
} finally {
outer.close();
}
这段代码有三个问题:
- 关闭顺序靠人记,写反了可能让内层资源在使用外层资源时被提前关掉。
- 如果
try块抛异常,finally里的close()也抛异常,后者会替换前者,原始错误丢失。 - 资源一多,嵌套层级迅速失控。
try-with-resources 就是把这套模板交给编译器生成。
AutoCloseable 与 Closeable
能被 try-with-resources 管理的对象必须实现 AutoCloseable:
public interface AutoCloseable {
void close() throws Exception;
}
Closeable 是它的子接口,把异常收窄成 IOException:
public interface Closeable extends AutoCloseable {
void close() throws IOException;
}
| 接口 | close() 声明 | 典型实现 |
|---|---|---|
AutoCloseable | throws Exception | 自定义资源、锁、作用域对象 |
Closeable | throws IOException | InputStream、OutputStream、Reader、Writer |
自定义资源实现 AutoCloseable 即可;如果关闭只涉及 IO,实现 Closeable 能让调用方的异常类型更精确。library 的 AuditLog 不涉及 IO,用 AutoCloseable;LoanBatch 包装 Writer,用 AutoCloseable 并把 close() 声明成 throws IOException。
基本语法与关闭顺序
try-with-resources 的写法:
try (AuditLog log = new AuditLog(sink, "borrow")) {
log.log("borrow 978-7-111-54742-6");
}
资源在 try 后面的括号里声明,作用域仅限于 try 块。块结束时,编译器生成的 finally 会调用 close(),即使块里抛异常也一样。
多个资源用分号隔开:
try (AuditLog outer = new AuditLog(sink, "outer");
AuditLog inner = new AuditLog(sink, "inner")) {
outer.log("borrow");
inner.log("write");
}
关闭顺序是声明的逆序:先关 inner,再关 outer。这像栈:后打开的资源可能依赖先打开的资源,所以必须先关。AuditLog 把打开和关闭都写进同一个 StringBuilder,运行结果能直接看到顺序:
StringBuilder sink = new StringBuilder();
try (AuditLog outer = new AuditLog(sink, "outer");
AuditLog inner = new AuditLog(sink, "inner")) {
outer.log("borrow");
inner.log("write");
}
System.out.print(sink);
本机 JDK 25 输出:
open outer
open inner
log outer: borrow
log inner: write
close inner
close outer
open 按声明顺序,close 按逆序。这个顺序不是约定,而是编译器生成的固定行为。
JDK 9 起,还可以把已有的 effectively final 变量放进 try-with-resources:
AuditLog log = new AuditLog(sink, "borrow");
try (log) {
log.log("borrow");
}
变量必须是事实 final,且类型实现 AutoCloseable。JDK 10 起还能用 var:
try (var log = new AuditLog(sink, "borrow")) {
log.log("borrow");
}
异常抑制:主异常不会被 close 覆盖
try-with-resources 最重要的行为是异常抑制(suppressed exception)。如果 try 块和 close() 都抛异常,try 块的异常成为主异常,close() 的异常被挂到主异常的 getSuppressed() 上。
先看一个两个方法都会失败的资源:
public final class FailingResource implements AutoCloseable {
private final String name;
public FailingResource(String name) {
this.name = name;
}
public void work() {
throw new IllegalStateException("work failed: " + name);
}
@Override
public void close() {
throw new IllegalStateException("close failed: " + name);
}
}
try (FailingResource resource = new FailingResource("db")) {
resource.work();
} catch (IllegalStateException e) {
System.out.println("primary = " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("suppressed = " + suppressed.getMessage());
}
}
本机 JDK 25 输出:
primary = work failed: db
suppressed = close failed: db
如果手写 finally,close() 抛出的 close failed 会替换 work failed,调用方看到的只是关闭失败,真正的业务错误反而丢了。try-with-resources 保留了两条信息,排查时可以顺着 getSuppressed() 看到全部。
只有一种情况 close() 的异常会成为主异常:try 块正常结束,close() 自己失败。这时没有主异常可挂,close() 的异常直接向外抛。
在 library 里落地
审计日志是一个最小资源:打开时记一行,关闭时再记一行。
public final class AuditLog implements AutoCloseable {
private final StringBuilder sink;
private final String name;
public AuditLog(StringBuilder sink, String name) {
this.sink = sink;
this.name = name;
sink.append("open ").append(name).append('\n');
}
public void log(String message) {
sink.append("log ").append(name).append(": ").append(message).append('\n');
}
@Override
public void close() {
sink.append("close ").append(name).append('\n');
}
}
它不持有外部资源,close() 不抛异常,适合演示顺序。真正写报表的 LoanBatch 持有 Writer:
public final class LoanBatch implements AutoCloseable {
private final Writer out;
private int rows;
public LoanBatch(Writer out) {
this.out = out;
}
public void write(Loan loan) throws IOException {
out.write(String.format("%d,%s,%s,%s,%s%n",
loan.id(), loan.isbn(), loan.memberId(),
loan.borrowedOn(), loan.dueOn()));
rows++;
}
public int rows() {
return rows;
}
@Override
public void close() throws IOException {
out.close();
}
}
LoanBatch 的契约是接管 out 的所有权:close() 会关闭调用方传进来的 Writer。如果调用方还想继续用这个 Writer,就不应该把它交给 LoanBatch,或者把 close() 改成只 flush()。资源所有权必须在 API 文档或方法名里说清楚,否则很容易出现「导出完报表,标准输出也被关了」的问题。
服务层导出借阅报表:
public void exportLoans(Writer out) throws IOException {
try (LoanBatch batch = new LoanBatch(out)) {
for (Loan loan : loans.findActive()) {
batch.write(loan);
}
}
}
exportLoans 声明 throws IOException,因为 LoanBatch.write 和 close 都可能失败。这是受检异常的合理位置:调用方是 CLI 或测试,能决定「报错退出」还是「重试」。异常一路传到 13.3 的 CLI 边界,再翻译成用户可见的错误。
运行导出:
LibraryService service = DemoData.service();
service.borrow("978-7-111-54742-6", "M001");
service.borrow("978-7-115-54608-1", "M002");
StringWriter buffer = new StringWriter();
service.exportLoans(buffer);
System.out.print(buffer);
本机 JDK 25 输出:
1,978-7-111-54742-6,M001,2026-10-13,2026-10-27
2,978-7-115-54608-1,M002,2026-10-13,2026-10-27
StringWriter 的 close() 是空操作,所以即使 LoanBatch 关闭了它,缓冲区内容仍然可以读取。换成 Files.newBufferedWriter 时,关闭会真正落盘,行为差异必须心里有数。
常见错误与边界
错误一:资源没有实现 AutoCloseable
try (String text = "x") {
System.out.println(text);
}
错误: 不兼容的类型: try-with-resources 不适用于变量类型
try (String text = "x") {
^
(String无法转换为AutoCloseable)
try-with-resources 不是「自动调用任何对象的某个方法」,它只认 AutoCloseable。包装类必须显式实现接口。
错误二:以为 close 的顺序按书写习惯来
很多人直觉是先声明的先关闭,实际正好相反。声明顺序是「打开顺序」,关闭顺序是它的逆序。依赖顺序的资源要按依赖关系声明:被依赖的放前面,依赖它的放后面。
错误三:在 try 块里 return 就忘了关闭
不需要手动处理。try-with-resources 生成的 finally 在 return 之前执行,资源照样关闭。这一点比自己写 finally 更可靠,因为 return、break、continue、异常都会触发关闭。
边界:不要管理不属于你的资源
System.out、调用方传入的 Connection、容器管理的 DataSource,这些资源的生命周期不由当前方法决定。把它们放进 try-with-resources 会提前关闭,导致后续代码失败。只对「这个方法打开、这个方法负责关闭」的资源使用 try-with-resources。
边界:close 不是幂等的
AutoCloseable 没有要求 close() 可以重复调用。try-with-resources 只调用一次;如果代码里又手动调用一次,某些实现会抛异常或重复释放。需要幂等关闭时,在自己的 close() 里加一个 closed 标志。
小结
- try-with-resources 只接受
AutoCloseable;Closeable是它的子接口,把close()的异常收窄为IOException。 - 多个资源按声明顺序打开、按逆序关闭;编译器生成的
finally保证return和异常路径都会关闭。 try块和close()同时失败时,try的异常是主异常,close()的异常挂在getSuppressed()上。AuditLog适合演示顺序,LoanBatch展示了带IOException的真实资源;exportLoans把关闭责任封装在服务层。- 资源所有权要显式约定:
LoanBatch关闭调用方的Writer,调用方就不能再使用它。 - 不要管理不属于自己的资源,不要假设
close()可以重复调用。
资源关闭解决的是「清理」,但错误怎么让用户看懂还没解决。底层可能抛 IOException、NumberFormatException、DateTimeParseException,CLI 不能把这些栈追踪直接甩给用户。下一节讲异常转换:在哪一层翻译、怎么保留原因、怎么设计用户可见的错误。
阅读导航:上一节:13.1 受检与非受检异常 · 下一节:13.3 异常转换与用户可见错误 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。