跳到主要内容

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)
) {
// ...
}

关闭顺序是:

  1. decoded
  2. buffered
  3. file

也就是声明顺序的逆序。这样上层包装器可以先刷新或完成自己的协议,再释放它依赖的底层资源。

如果某个资源初始化失败,已经成功创建的前序资源会被关闭,尚未创建的资源不会参与关闭。

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();
}

顺序是:

  1. 执行 try 主体。
  2. 自动关闭资源。
  3. 进入匹配的 catch
  4. 执行外层 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 CloseableAutoCloseable 有什么区别

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 应在命名或文档中说明资源是借用还是移交。

包装流关闭通常会继续关闭底层流,因此不能只看最外层对象。需要保留底层连接时,应调整所有权设计或使用明确不传播关闭的包装方案。