如何优雅地关闭资源
很多时候我们都会用到io资源,比如文件、网络、各种连接等。比如有时候我们需要从一个文本文件中读取数据,一般的步骤是:
- 用FileReader打开文件
- 包装成BufferReader
- 循环地从BufferReader中读取内容,直接读出来的内容为空
- 关闭BufferReader和FileReader
用代码实现如下:
FileReader fileReader = null;
BufferedReader bufferedReader = null;
try {
fileReader = new FileReader("test.txt");
bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileReader != null) {
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
当然做法不止这一种,也可以通过字节流的方式读入,再包装成可以直接读取的方式。但是无论怎么样,我们都需要在finally块中一个个地把各种资源关闭,因为变量作用域的原因,在关闭之前还得判空,于是我们的代码就变成了上面的那样。
太繁琐太不优雅了,你可能会想到,直接把异常一股脑地抛出去不就行了吗?不行,因为调用方只是想读个文件而已,你却要它来处理一大堆异常?而且,就算调用方来处理异常,那么调用方的代码会变得更不优雅,因为还要针对各种不同类型的异常来处理。
下面介绍一种非常优雅的做法,直接上代码:
try (
FileReader fileReader = new FileReader("test.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);
) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
结构就是
try(){...}catch(...) {...}
和第一种写法最大的区别就是try关键字后多了一对括号。具体用法是,在try后的括号内声明并且定义各种资源,可以是一个也可以是多个,然后别的东西不用变,资源也不用你来关闭了。
可能你会有疑问,fileReader和bufferedReader是真的关闭了吗?首先告诉你,是的。但是怎么验证呢?
我们先关注另一个问题,try后面的括号内是用来定义各种资源的,也就是初始化,是不是所有的东西都可以在里面初始化?比如下面的String line可不可以?
代码编译出错了:需要AutoCloseable类型但是提供了String类型,AutoCloseable是一个接口,最早出现在jdk1.7中。直接贴一段该接口的说明
An object that may hold resources (such as file or socket handles) until it is closed. The {@link #close()} method of an {@code AutoCloseable} object is called automatically when exiting a {@code try}-with-resources block for which the object has been declared in the resource specification header. This construction ensures prompt release, avoiding resource exhaustion exceptions and errors that may otherwise occur.
最核心的一点就是当退出try块,这些资源会自动调用close方法。这样就达到了自动关闭资源的目的,如果去看FileReader和BufferedReader的源码,它们都间接实现了AutoCloseable接口。
为了验证这一点,我们自己写一个实现AutoCloseable的类:
public class Auto implements AutoCloseable {
public void run() {
System.out.println("run");
}
@Override
public void close() throws Exception {
System.out.println("close");
}
}
然后在测试类中生成并且调用run方法
public class Main {
public static void main(String[] args) {
try (Auto auto = new Auto()){
auto.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行后的结果如下:
run
close
上面的测试类并没有显式调用close方法,但是最后这个方法被调用了。到这里我们可以猜测是在编译的时候,javac命令已经帮我们把调用close方法的指令自动生成了,我们用javap命令反编译来看一下:
C:\>javap -c Main.class
Compiled from "Main.java"
public class source.Main {
public source.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class source/Auto
3: dup
4: invokespecial #3 // Method source/Auto."<init>":()V
7: astore_1
8: aconst_null
9: astore_2
10: aload_1
11: invokevirtual #4 // Method source/Auto.run:()V
14: aload_1
15: ifnull 85
18: aload_2
19: ifnull 38
22: aload_1
23: invokevirtual #5 // Method source/Auto.close:()V
26: goto 85
29: astore_3
30: aload_2
31: aload_3
32: invokevirtual #7 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
35: goto 85
38: aload_1
39: invokevirtual #5 // Method source/Auto.close:()V
42: goto 85
45: astore_3
46: aload_3
47: astore_2
48: aload_3
49: athrow
50: astore 4
52: aload_1
53: ifnull 82
56: aload_2
57: ifnull 78
60: aload_1
61: invokevirtual #5 // Method source/Auto.close:()V
64: goto 82
67: astore 5
69: aload_2
70: aload 5
72: invokevirtual #7 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
75: goto 82
78: aload_1
79: invokevirtual #5 // Method source/Auto.close:()V
82: aload 4
84: athrow
85: goto 93
88: astore_1
89: aload_1
90: invokevirtual #9 // Method java/lang/Exception.printStackTrace:()V
93: return
Exception table:
from to target type
22 26 29 Class java/lang/Throwable
10 14 45 Class java/lang/Throwable
10 14 50 any
60 64 67 Class java/lang/Throwable
45 52 50 any
0 85 88 Class java/lang/Exception
}
果不其然,在run方法后面调用了Auto.close方法。这里值得说的是出现了多个close方法的调用,原因就是,这里的处理其实相当于在一个finally块中调用close方法,为了保证finally中的语句会执行,会在抛出异常的后面再执行finally中的语句,也就是close方法,最后才return。