一、背景
在 Java 编程中,如果打开了外部资源,如:文件输入输出流、数据库连接、网络连接(InputStream、OutputStream、java.sql.Connection)等,那我们必须在这些外部资源使用完后,去手动地关闭它们。因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制。如果我们在编程时并没有确保在正确的时机关闭外部资源,就会导致外部资源泄露,就会出现文件被异常占用,数据库连接过多导致连接池溢出等诸多严重的性能问题。
而关闭的方法有很多种。比如:finalizer、try-catch-finally、try-with-resources 等等。
finalizer 机制可以关闭,但是其执行性不可预测,还有可能造成内存泄漏,所以一般不使用。因此,选择就落在了 try-catch-finally 和 try-with-resources 之间。
二、使用 try-catch-finally 关闭资源
为了确保外部资源一定要被关闭,通常关闭代码被写入 finally 代码块中。当然,我们还必须注意到关闭资源时可能抛出的异常,于是便有了下面的代码:
public class FileTestTwo {
public static void main(String[] args) {
FileInputStream in = null;
int length = 0;
try {
in = new FileInputStream("test.txt");
byte[] bytes = new byte[1024];
while ((length = in.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, length));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
上述代码:通过文件输入流读取 test.txt 文件中的内容,一次读取 1024B,每读取一次,就将读取到的内容打印到屏幕中去,最后在 finally 语句中关闭流。
再在原有的代码上加一个文件输出流。如下:
public class FileTestTwo {
public static void main(String[] args) {
FileInputStream in = null;
FileOutputStream out = null;
int length = 0;
try {
in = new FileInputStream("test.txt");
out = new FileOutputStream("test2.txt");
byte[] bytes = new byte[1024];
while ((length = in.read(bytes)) != -1) {
out.write(bytes, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
} finally { // 在这里使用 finally,防止 in.close() 方法抛出异常,而没有执行 out.close()
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
}
从上面代码来看,打开的资源越多,需要关闭资源的代码就越多,甚至会超过业务代码。因为我们不仅需要关闭 FileInputStream
,还需要保证在执行 in.close();
代码时出现了异常,FileOutputStream
也能正确地被关闭。所以,不得不嵌套 finally 语句。
但值得注意的是:打开的资源越多,finally 语句就嵌套得越深!!
三、使用 try-with-resources 关闭资源
在 JDK1.7 中,新增了 try-with-resources 语法,才实现了自动关闭外部资源的功能。
使用它时,必须要求外部资源的句柄对象(如:FileInputStream 对象)实现了 AutoCloseable
接口。
那么,如何使用它呢?
将外部资源的句柄对象的创建放在 try 后面的括号中,当这个 try-catch 代码块执行完毕后,Java 会确保外部资源的close() 方法被调用。
将上述代码使用 try-with-resources 语法进行改造,代码如下:
public class FileThree {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream("test.txt");
FileOutputStream out = new FileOutputStream("test2.txt")) {
int length = 0;
byte[] bytes = new byte[1024];
while ((length = in.read(bytes)) != -1) {
out.write(bytes, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这样看上去,是不是感觉代码干净了许多。当程序运行完离开try语句块时,( ) 里的资源就会被自动关闭。
但是 try-with-resources 这几个关键点要记住:
- try() 里面的类,必须实现了 AutoCloseable 接口
- 在 try() 代码中声明的资源被隐式声明为 final
- 使用分号分隔,可以声明多个资源
手动自定义
自己新建一个类,配合使用 try-with-resources 语法。
那么该类需要:实现 AutoClosable
接口。该接口的实现类需要重写 close()
方法。代码如下:
public class TestAutoClosable implements AutoCloseable {
public void test() {
System.out.println("test() 方法被调用");
}
@Override
public void close() throws Exception {
System.out.println("close() 方法被调用");
}
public static void main(String[] args) {
try (TestAutoClosable test = new TestAutoClosable()) {
test.test();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行上述代码:
test() 方法被调用
close() 方法被调用
发现 TestAutoClosable#close() 方法被调用了(代码中是没有显示调用这个 close() 方法的哈)。
那么,这个是怎么做到的呢?
这个就得看看 try-with-resources 的实现原理了
实现原理
try-with-resources 并不是 JVM 的新增功能,只是 JDK 实现了一个语法糖。当你将上面代码反编译后会发现,其实对JVM 而言,它看到的依然是之前的写法:
public class TestAutoClosable implements AutoCloseable {
public TestAutoClosable() {
}
public void test() {
System.out.println("test() 方法被调用");
}
public void close() throws Exception {
System.out.println("close() 方法被调用");
}
public static void main(String[] args) {
try {
TestAutoClosable test = new TestAutoClosable();
Throwable var2 = null;
try {
test.test();
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (test != null) {
if (var2 != null) {
try {
test.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
test.close();
}
}
}
} catch (Exception var14) {
var14.printStackTrace();
}
}
}
从反编译后的代码来看,编译器自动帮我们生成了 finally 块,并且在里面调用了资源的 close() 方法,所以例子中的close() 方法会在运行的时候被执行。
异常屏蔽
从反编译的代码中看到了一个 addSuppressed()
方法,那么,这个方法的作用是什么呢?
为了搞清楚它,我们修改下上面的例子,如下代码:
public class TestAutoClosable implements AutoCloseable {
public void test() throws Exception{
throw new Exception("test() 方法被调用");
}
@Override
public void close() throws Exception {
throw new MyException("close() 方法被调用");
}
public static void testClose() throws Exception{
TestAutoClosable test = null;
try {
test = new TestAutoClosable();
test.test();
} finally {
if (null != test) {
test.close();
}
}
}
// 测试
public static void main(String[] args) {
try {
testClose();
} catch (Exception e) {
e.printStackTrace();
}
}
}
最后,控制台打印出:
java.lang.Exception: close() 方法被调用
at com.tiandy.zzc.design.file.TestAutoClosable.close(TestAutoClosable.java:20)
at com.tiandy.zzc.design.file.TestAutoClosable.testClose(TestAutoClosable.java:45)
at com.tiandy.zzc.design.file.TestAutoClosable.main(TestAutoClosable.java:31)
问题来了,由于我们一次只能抛出一个异常,所以在最上层看到的是最后一个抛出的异常——也就是 close() 方法抛出的 Exception ,而 抛出的 test() 方法的Exception 被忽略了。这就是所谓的异常屏蔽。
由于异常信息的丢失,异常屏蔽可能会导致某些bug变得极其难以发现。为了解决这个问题,从 Java 1.7 开始,大佬们为 Throwable 类新增了 addSuppressed() 方法,支持将一个异常附加到另一个异常身上,从而避免异常屏蔽。
那么被屏蔽的异常信息会通过怎样的格式输出呢?
我们再运行一遍刚才用 try-with-resource 包裹的 main() 方法:
public static void main(String[] args) {
try (TestAutoClosable test = new TestAutoClosable()) {
test.testClose();
} catch (Exception e) {
e.printStackTrace();
}
}
控制台打印出:
java.lang.Exception: close() 方法被调用
at com.tiandy.zzc.design.file.TestAutoClosable.close(TestAutoClosable.java:20)
at com.tiandy.zzc.design.file.TestAutoClosable.testClose(TestAutoClosable.java:45)
at com.tiandy.zzc.design.file.TestAutoClosable.main(TestAutoClosable.java:25)
Suppressed: java.lang.Exception: close() 方法被调用
at com.tiandy.zzc.design.file.TestAutoClosable.close(TestAutoClosable.java:20)
at com.tiandy.zzc.design.file.TestAutoClosable.main(TestAutoClosable.java:26)
可以看到,异常信息中多了一个 Suppressed 的提示,告诉我们这个异常其实由两个异常组成,close() 方法的 Exception 是被 Suppressed 的异常。
总结
处理必须关闭的资源时,始终要优先考虑使用 try-with-resources,而不是 try-finally。这样得到的代码将更简洁,清晰,产生的异常也更有价值,这些也是 try-finally 无法做到的。