Jakob Jenkov 2019-08-25
Java 的 try-with-resources(带资源的 try)结构是一种异常处理机制,可以在你使用完诸如 InputStream 或 JDBC Connection 等资源后自动关闭它们。为此,你必须在 try-with-resources 块中打开并使用这些资源。当执行离开该块时,无论是否抛出异常(无论是来自 try 块内部,还是在关闭资源时),所有在 try-with-resources 块中打开的资源都会被自动关闭。
Try-with-resources 基础用法
让我们通过一个例子来了解 try-with-resources 是如何工作的:
private static void printFile() throws IOException {
try(FileInputStream input = new FileInputStream("file.txt")) {
int data = input.read();
while(data != -1){
System.out.print((char) data);
data = input.read();
}
}
}
这个例子展示了如何在 try-with-resources 块中打开一个 FileInputStream,从中读取数据,并在执行离开 try 块后自动关闭该流(无需显式调用 close())。
注意方法中的第一行:
try(FileInputStream input = new FileInputStream("file.txt")) {
这就是 try-with-resources 结构。FileInputStream 变量在 try 关键字后的括号内声明,同时被实例化并赋值。
当 try 块执行完毕后,FileInputStream 会自动关闭。之所以能实现这一点,是因为 FileInputStream 实现了 Java 接口 java.lang.AutoCloseable。所有实现了该接口的类都可以用于 try-with-resources 结构中。
Java 9 对 Try-with-resources 的增强
在 Java 9 之前,要自动关闭的资源必须在 try 语句的括号内创建。但从 Java 9 开始,这一限制被放宽了:只要引用资源的变量是 effectively final(实际上不可变),就可以直接在括号中引用该变量。
示例(Java 9+):
private static void printFile() throws IOException {
FileInputStream input = new FileInputStream("file.txt");
try(input) {
int data = input.read();
while(data != -1){
System.out.print((char) data);
data = input.read();
}
}
}
注意:input 变量现在在 try 块外部声明和初始化,但在 try 括号中被引用。Java 仍会在 try 块结束时正确关闭它。
使用多个资源
你可以在一个 try-with-resources 块中使用多个资源,它们都会被自动关闭:
private static void printFile() throws IOException {
try(
FileInputStream input = new FileInputStream("file.txt");
BufferedInputStream bufferedInput = new BufferedInputStream(input)
) {
int data = bufferedInput.read();
while(data != -1){
System.out.print((char) data);
data = bufferedInput.read();
}
}
}
此例中创建了两个资源:FileInputStream 和 BufferedInputStream。两者都会在离开 try 块时自动关闭。
资源的关闭顺序
在 try-with-resources 中声明的资源,会按照 与声明顺序相反的顺序 被关闭。
例如,在上一节的例子中:
- 先关闭
BufferedInputStream - 再关闭
FileInputStream
这是为了确保包装流(如 BufferedInputStream)先被关闭,底层流(如 FileInputStream)后被关闭,符合资源依赖逻辑。
自定义 AutoCloseable 实现
try-with-resources 不仅适用于 Java 内置类,你也可以在自己的类中实现 AutoCloseable 接口。
AutoCloseable 接口定义如下:
public interface AutoCloseable {
public void close() throws Exception;
}
下面是一个简单实现:
public class MyAutoClosable implements AutoCloseable {
public void doIt() {
System.out.println("MyAutoClosable doing it!");
}
@Override
public void close() throws Exception {
System.out.println("MyAutoClosable closed!");
}
}
使用方式:
private static void myAutoClosable() throws Exception {
try(MyAutoClosable myAutoClosable = new MyAutoClosable()){
myAutoClosable.doIt();
}
}
输出结果:
MyAutoClosable doing it!
MyAutoClosable closed!
这表明 try-with-resources 是一种非常强大的机制,无论资源是你自定义的还是 Java 内置的,都能确保正确关闭。
Try-with-resources 的异常处理机制
try-with-resources 的异常处理语义与传统的 try-catch-finally 有所不同。理解这些差异有助于编写更健壮的代码。
基本规则:
- 如果 try 块内抛出异常 → 所有资源仍会被自动关闭,然后该异常被向上抛出。
- 如果关闭资源时抛出异常 → 其他资源仍会继续关闭;第一个关闭失败的异常会被抛出,其余异常被“抑制”(suppressed)。
- 如果 try 块和关闭过程都抛出异常 → try 块中的异常会被抛出,关闭时的异常会被抑制(添加到主异常的
getSuppressed()数组中)。
这与传统
try-catch-finally不同:在 finally 中抛出的异常会覆盖 try 块中的异常。而 try-with-resources 优先保留业务逻辑中的异常。
示例:自定义可抛异常的资源
public class AutoClosableResource implements AutoCloseable {
private String name = null;
private boolean throwExceptionOnClose = false;
public AutoClosableResource(String name, boolean throwExceptionOnClose) {
this.name = name;
this.throwExceptionOnClose = throwExceptionOnClose;
}
public void doOp(boolean throwException) throws Exception {
System.out.println("Resource " + this.name + " doing operation");
if(throwException) {
throw new Exception("Error when calling doOp() on resource " + this.name);
}
}
@Override
public void close() throws Exception {
System.out.println("Resource " + this.name + " close() called");
if(this.throwExceptionOnClose){
throw new Exception("Error when trying to close resource " + this.name);
}
}
}
单资源测试
public static void tryWithResourcesSingleResource() throws Exception {
try(AutoClosableResource resourceOne = new AutoClosableResource("One", false)) {
resourceOne.doOp(false);
}
}
- 若
doOp(true)→ 抛出异常,close()异常(如有)被抑制。 - 若
doOp(false)但throwExceptionOnClose=true→close()异常被抛出,getSuppressed()为空。
双资源测试
public static void tryWithResourcesTwoResources() throws Exception {
try(
AutoClosableResource resourceOne = new AutoClosableResource("One", true);
AutoClosableResource resourceTwo = new AutoClosableResource("Two", true)
){
resourceOne.doOp(true); // 抛出异常
resourceTwo.doOp(false);
}
}
结果:
- 主异常:来自
resourceOne.doOp(true) - 抑制异常(suppressed):两个
close()抛出的异常(按关闭顺序:先 Two,后 One)
注意:一旦 try 块抛出异常,后续代码不再执行,因此
resourceTwo.doOp(false)实际不会运行。
Catch 块的使用
你可以在 try-with-resources 后添加 catch 块,就像普通 try 一样:
try(AutoClosableResource resourceOne = new AutoClosableResource("One", true)) {
resourceOne.doOp(true);
} catch(Exception e) {
Throwable[] suppressed = e.getSuppressed();
throw e;
}
- 如果
doOp()抛异常 → 被 catch 捕获,getSuppressed()包含关闭时的异常。 - 如果仅
close()抛异常 → 也被 catch 捕获,但getSuppressed()为空。
Finally 块的使用
你也可以添加 finally 块,它会在 catch 之后执行(如果存在):
try(AutoClosableResource resourceOne = new AutoClosableResource("One", true)) {
resourceOne.doOp(false);
} catch(Exception e) {
throw e;
} finally {
throw new Exception("Hey, an exception from the finally block");
}
⚠️ 警告:如果在 finally 块中抛出异常,之前所有的异常(包括 try 和 catch 中的)都会丢失! 它们不会被抑制,也无法通过 getSuppressed() 获取。
因此,应避免在 finally 中抛出异常,或谨慎处理。
手动添加被抑制的异常
你可以使用 Throwable.addSuppressed() 方法手动将异常标记为“被抑制”:
Exception finalException = null;
try(AutoClosableResource resourceOne = new AutoClosableResource("One", true)) {
resourceOne.doOp(false);
} catch(Exception e) {
finalException = new Exception("Error...");
finalException.addSuppressed(e);
for(Throwable suppressed : e.getSuppressed()){
finalException.addSuppressed(suppressed);
}
} finally {
if(finalException != null){
throw finalException;
}
}
通常不需要手动操作,但在某些高级场景下可能有用。
旧式资源管理(Java 7 之前)
在 Java 7 引入 try-with-resources 之前,资源管理非常繁琐且容易出错:
private static void printFile() throws IOException {
InputStream input = null;
try {
input = new FileInputStream("file.txt");
int data = input.read();
while(data != -1){
System.out.print((char) data);
data = input.read();
}
} finally {
if(input != null){
input.close(); // close() 本身也可能抛异常!
}
}
}
问题:
- 如果
try块抛异常,finally中close()也抛异常 → 只有 close() 的异常被传播,原始异常丢失。 - 需要手动判空、嵌套 try-catch,代码冗长。
try-with-resources 正是为了解决这些问题而设计的。
总结
- try-with-resources 自动管理实现了
AutoCloseable的资源。 - Java 9+ 支持引用 effectively final 的外部变量。
- 异常优先级:try 块异常 > close() 异常(后者被抑制)。
- 关闭顺序:后声明的资源先关闭。
- 避免在 finally 中抛异常,否则会掩盖重要异常。