Effective-Java-9try-with-resources优先于try-finally

Java 类库中包括许多必须通过调用 close 方法来手工关闭的资源。例如 InputStream、OutputStream 和 java.sql.Connection。客户端经常会忽略资源的关闭。虽然这其中的许多资源都是用终结方法作为安全网,但是效果并不理想(见第8条)。

根据经验,try-finally 语句是确保资源会被适时关闭的最佳方法,就算发生异常或者返回也一样:

1
2
3
4
5
6
7
8
public static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}

这看起来似乎没有什么问题,但如果再加入一个资源,就会变得糟糕了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void copy(String src, String dst) throws IOException {
FileInputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}

这可能让人难以置信,不过就算优秀的程序员也经常犯这样的错误。Joshua Bloch (本书作者) 在《Java Puzzlers》[Bloch5] 第88页犯过该错误,时隔多年都无人发现。事实上,在 2007年,close 方法在 Java 类库中有 2/3 都用错了。

即使用 try-finally 语句正确地关闭了资源 (如前两段代码),依然存在许多不足。因为在 try 块和 finally 块中的代码,都会抛出异常。例如,在 firstLineOfFile 中,如果因为物理设备损坏,那么调用 readLine、close 就会抛出异常。这种情况下第二个异常完全抹除了第一个异常。在异常堆栈轨迹中,完全没有第一个异常的记录,这会导致调试变得非常复杂,因为通常需要看到第一个异常才能诊断出问题何在,虽然可以通过编写代码来禁止第二个异常,保留第一个异常,但是实现起来太繁琐了。

当 Java 7 引入 try-with-resources 语句时 [JLS,14.20.3],所有这些问题一下子就全部解决了。要使用这个构造的资源,必须先实现 AutoCloseable 接口,其中包括了单个返回 void 的 close 方法。Java 类库与第三方类库中的许多类和接口,现在都实现或扩展了 AutoCloseable 接口。如果编写了一个类,它代表的是必须被关闭的资源,那么这个类也应该实现 AutoCloseable。

以下是使用 try-with-resources 的两个范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// try-with-resources - the best way to close resources!
public static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}

// try-with-resources on multiple resources - short and sweet
public static void copy(String src, String dst) throws IOException {
try (FileInputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}

使用 try-with-resources 不仅使代码变得更简洁易懂,也更容易进行诊断。以 firstLineOfFile 为例,如果调用 readLine 和 (不可见的) close 方法都抛出异常,后一个异常就会被禁止,以保留第一个异常。事实上,为了保留你想看到的那个异常,即使是多个异常都可以被禁止。这些异常禁止并不是被简单的抛弃了,而是会被打印在堆栈轨迹中,并注明它们是被禁止的异常。通过编程调用 getSuppressed 方法可以访问到它们,getSuppressed 方法也已经添加在 Java 7 的 Throwalble 中了。

在 try-with-resources 语句中还可以使用 catch 子句,就像在平时的 try-finally 语句中一样。这样既可以处理异常,又不需要再套一层代码。

该 firstLineOfFile 方法没有抛出异常,但如果他无法打开文件,或者无法从中读取,就会返回一个默认值:

1
2
3
4
5
6
7
8
// try-with-resources with a catch clause
public static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}

在处理必须关闭的资源时,始终优先考虑 try-with-resources ,而不是用 try-finally。这样得到的代码将更加简洁、清晰。产生的异常也更有价值,这是 try-finally 不能做到的。