try-with-resources 与关闭语义
try-with-resources 会在控制流离开代码块时自动关闭资源,包括正常返回和异常传播。它还规定了多个资源的关闭顺序,以及业务异常和关闭异常同时出现时应保留哪一个。
1. 哪些对象可以自动关闭
资源类型需要实现 AutoCloseable:
public interface AutoCloseable {
void close() throws Exception;
}
文件流、Socket、JDBC 连接、语句和结果集都实现了 AutoCloseable 或它的子接口 Closeable。
static String read(Path path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path)) {
return reader.readLine();
}
}
无论 readLine() 正常返回还是抛出异常,reader.close() 都会在方法真正返回或异常继续传播前执行。
1.1 已有的有效 final 变量也能作为资源
BufferedReader reader = Files.newBufferedReader(path);
try (reader) {
return reader.readLine();
}
这里的变量必须是 final 或有效 final,即赋值后不再改变。资源块结束后,变量仍在词法作用域中,但对象已经关闭,不应再次使用。
2. 多个资源按声明的逆序关闭
资源通常存在依赖关系,后创建的包装层依赖先创建的底层资源:
try (
InputStream file = Files.newInputStream(path);
InputStream buffered = new BufferedInputStream(file);
Reader decoded = new InputStreamReader(buffered, StandardCharsets.UTF_8)
) {
// ...
}
关闭顺序是:
decodedbufferedfile
也就是声明顺序的逆序。这样上层包装器可以先刷新或完成自己的协议,再释放它依赖的底层资源。
如果某个资源初始化失败,已经成功创建的前序资源会被关闭,尚未创建的资源不会参与关闭。
2.1 JDBC 资源按依赖顺序声明
try (
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet resultSet = statement.executeQuery()
) {
while (resultSet.next()) {
// ...
}
}
退出时先关闭 ResultSet,再关闭 PreparedStatement,最后关闭或归还 Connection。声明顺序与资源依赖一致,关闭顺序自然正确。
3. 关闭异常怎样保留
方法体和 close() 都可能抛异常。try-with-resources 会把方法体中的异常作为主要异常,把关闭期间的异常记录为受抑制异常:
try (FailingResource resource = new FailingResource()) {
throw new IllegalStateException("operation failed");
}
假设 close() 又抛出 IOException("close failed"),最终传播的是 IllegalStateException,关闭异常可以通过下面的方法读取:
for (Throwable suppressed : exception.getSuppressed()) {
logger.warn("resource close also failed", suppressed);
}
主要业务失败不会被清理失败覆盖,同时诊断信息仍然保留。
3.1 手写 finally 容易覆盖原始异常
Resource resource = open();
try {
resource.use();
} finally {
resource.close();
}
如果 use() 和 close() 都失败,finally 中的关闭异常会覆盖原始业务异常。手动正确实现受抑制异常需要更多分支,普通资源管理应直接使用 try-with-resources。
3.2 多个 close 都失败时
资源按逆序关闭。最先发生的关闭异常成为主要异常,后续关闭异常加入它的 suppressed 列表。如果方法体已经抛异常,所有关闭异常都作为该业务异常的 suppressed 异常。
即使一个资源关闭失败,其余资源仍会继续尝试关闭。
4. catch 和 finally 在资源关闭后执行
try (Resource resource = open()) {
resource.use();
} catch (IOException exception) {
handle(exception);
} finally {
finishRequest();
}
顺序是:
- 执行 try 主体。
- 自动关闭资源。
- 进入匹配的
catch。 - 执行外层
finally。
因此 catch 看到的异常已经包含关闭阶段产生的 suppressed 异常。
从 try 主体 return 也会先关闭资源,再完成返回。如果 close() 抛异常,原本准备返回的值不会正常返回。
5. 资源所有权决定谁负责关闭
“实现了 AutoCloseable 就立即关闭”并不是完整规则。代码只应关闭自己创建或明确接管所有权的资源。
void writeReport(OutputStream output) throws IOException {
// output 由调用方传入,方法不擅自关闭
output.write(reportBytes);
}
如果方法把调用方传入的 OutputStream 放进 try-with-resources,调用结束后可能把共享 Socket 或响应流一起关闭。API 应明确:
- 方法只是借用资源,调用方继续拥有并负责关闭。
- 方法接管所有权,返回后资源一定关闭。
- 方法返回一个新资源,调用方成为所有者。
包装流的 close() 通常会关闭底层流。只希望完成编码或压缩但保留底层连接时,需要使用不会传播关闭的包装策略,或者调整协议边界,不能假设只关闭最外层而不影响内部。
6. 自定义 AutoCloseable
需要成对获取和释放的资源可以实现 AutoCloseable:
public final class LockGuard implements AutoCloseable {
private final Lock lock;
private boolean closed;
private LockGuard(Lock lock) {
this.lock = lock;
lock.lock();
}
public static LockGuard acquire(Lock lock) {
return new LockGuard(lock);
}
@Override
public void close() {
if (!closed) {
closed = true;
lock.unlock();
}
}
}
调用方可以把锁的作用域写进语法结构:
try (LockGuard ignored = LockGuard.acquire(lock)) {
updateSharedState();
}
自定义关闭逻辑应明确是否允许重复调用、是否可能阻塞,以及失败时抛出什么异常。AutoCloseable 本身不要求 close() 幂等,具体类型需要记录自己的契约。
7. 常见问题
7.1 try-with-resources 是否等同于 finally 调用 close
目标相同,但生成的控制流还会处理资源初始化失败、逆序关闭和 suppressed 异常。只写一句“编译器加了 finally”会遗漏最重要的异常语义。
7.2 Closeable 与 AutoCloseable 有什么区别
Closeable 继承 AutoCloseable,其 close() 声明抛出 IOException,并要求资源已经关闭时再次调用没有效果。AutoCloseable.close() 可以声明更宽的 Exception,接口本身不要求幂等。
7.3 close 失败可以忽略吗
不一定。输出流关闭失败可能表示缓冲数据没有完整写出,数据库连接归还失败可能影响连接池。读取场景和写入场景对关闭失败的风险不同。至少应让异常进入现有错误处理与监控,不能统一空 catch。
7.4 为什么资源不能只交给 GC
文件描述符、Socket 和数据库连接是有限的外部资源,何时释放不能依赖对象何时被垃圾回收。GC 管理 Java 对象可达性,不提供及时的协议关闭、刷新或归还连接保证。
8. 面试题
8.1 try-with-resources 怎样保证资源关闭
出现公司:蚂蚁金服、中科创达
考察重点
- 资源需要实现什么接口。
- 多个资源的关闭顺序。
- 主体异常与关闭异常怎样同时保留。
相关内容:第 1 节“哪些对象可以自动关闭”、第 2 节“多个资源按声明的逆序关闭”、第 3 节“关闭异常怎样保留”。
参考回答
try-with-resources 接收实现 AutoCloseable 的资源。当控制流离开 try 主体时,无论正常返回还是抛异常,资源都会按声明的逆序关闭;初始化中途失败时,已经创建的资源也会关闭。
如果主体和 close() 同时失败,主体异常继续作为主要异常,关闭异常通过 getSuppressed() 保留。多个关闭异常也不会阻止后续资源继续关闭。这比手写一个简单 finally 更完整。
8.2 方法是否应该关闭调用方传入的流
出现公司:网易
考察重点
- 资源所有权与生命周期由谁控制。
- 包装流关闭是否会传播到底层。
- API 怎样明确借用与接管关系。
相关内容:第 5 节“资源所有权决定谁负责关闭”。
参考回答
通常只关闭当前方法创建或明确接管所有权的资源。调用方传入的流可能还要继续写入,方法擅自关闭会破坏更大的协议边界。API 应在命名或文档中说明资源是借用还是移交。
包装流关闭通常会继续关闭底层流,因此不能只看最外层对象。需要保留底层连接时,应调整所有权设计或使用明确不传播关闭的包装方案。