Java中关闭流一直是个让人头疼的问题,很多人要么就是忘记关闭,要么就是对关闭的顺序模糊不清,实际开发过程中,也常常因为流未关闭导致应用程序出现各种莫名奇妙的Bug。所以本篇整理出了Java中Stream流(主要指IO流,非指Java1.8中流式处理)关闭中常见的几种情况,并通过自己测试和查看源码等方式给出证明过程,下面开始。
1.各种流中存在包装关系,如何关闭
try {
InputStream inputStream = new FileInputStream("D:\\test\\test.txt");
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
System.out.println(bufferedReader.readLine());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
inputStream.close();
inputStreamReader.close();
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
这段代码相信大家应该都很熟悉,java中按行读取文件中的内容。这里面用到了FileInputStream,InputStreamReader和BufferedReader这3个类,主要用了BufferedReader 的readLine方法。那么问题来了,这里FileInputStream,InputStreamReader和BufferedReader这3个类都有close方法,这3个方法是否都需要关闭,以及关闭的顺序是怎么样的呢?
我们通过查看BufferdReader的close的源码可以看到:
这里是是判断了in是否为空,如果不为空则调用in的close方法。这里的in指的就是前面代码中 BufferedReader中的构造函数传入的InputStreamReader。
所以我们可以看一下InputStreamReader的构造函数和close方法:
构造函数:
close方法:
可以看到这里新创建了一个StreamDecoder类,close方法其实调用的也是StreamDecoder中的close方法。所以我们继续看StreamDecoder类的close方法.
我们可以看到这里其实调用的也是in的close方法(in是最前面传入的FileInputStream,ch在本文例子中没有用到,是null值)。
所以我们总结后发现,其实这里面只需要关闭最外层的BufferedReader 即可。
那么问题来了,在这个例子中,我如果只关闭inputStrem或者inputReadStream,或者我3个类都调用了close方法会发生什么呢?
我们通过上面的源码分析如果是3个类都调用close方法的话,因为包装类有ensureOpen操作,所以最终只会调用一次,不过我们可以通过源码看到InputStream继承了Closeable接口
这里面注释有写到如果流已经关闭,再次调用也不会受到影响。所以就算3个方法都调用了close方法,其实等同于只调用1次close。
那么如果只关闭inputStream会如何呢?我们看实际运行的结果:
这里我们看到只关闭InputStream也是可以的,这里主要是BufferedReader和InputReader写得比较严谨,做了ensureOpen验证。其实3个类始终操作的都是同一个InputStream,所以如果读者想自己写操作InputStream的工具类,一定要记得做ensureOpen验证,并且如果工具类中有多个包装关系,要确保包装中的所有类的close方法都能实现安全关闭的效果。
此次我贴一张网上找来的图:
我们发现,所有的Reader和Writer都符合上述分析。
所以,如果读者使用jdk内置的文件读写工具类,都符合上述分析的特点,只需要关闭任一包装类的流即可,如果是用第三方使用的开源库,则需要读者自己确认流是否已经关闭。
2.关于流传输中先后关闭的问题
我们看另外一个例子:
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("D:\\temp\\test.txt");
out = new FileOutputStream("D:\\temp\\test1.txt");
byte[] bytes = new byte[1024 * 8];
int len = 0;
while ((len = in.read(bytes)) != -1) {
out.write(bytes, 0, len);
out.flush();//刷新缓冲区
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
这个例子中网上有人说需要先关闭in,再关闭out,个人理解,两者关闭的先后顺序无影响,但找不到反驳的理由。。
3.AutoCloseable 接口
当然Jdk在1.7版本的时候给出了更优雅的解决方案解决上面。我们看到前面的Closeable实现了AutoCloseable接口:
这个AutoCloseable的用法是这样的:
try(Resource resource1 = new Resource("1");
Resource resource2 = new Resource("2");
Resource resource3 = new Resource("3");){
}
catch (Exception e){
}
Resource 类:
public class Resource implements Closeable {
private String name;
public Resource(String name){
this.name = name;
}
@Override
public void close() throws IOException{
System.out.println(name+"已经关闭");
throw new IOException();
}
}
运行结果:
此处可以看到,try-catch的用法有两个特点,关闭顺序是按定义的顺序,后定义先释放的原则。另外一个特点是自动捕获了IOException异常。
通过分析,我们之前提到的内置IO工具类,都可以直接使用这个写法,如果是使用第三方或者自己写的工具类,则需要看是否满足先后顺序的特点。
总结:
1.如果是用内置的Reader和Writer,那么直接关闭最外层的Reader和Writer的close方法即可
2.流传输的过程中,只要流数据都传输完成,先关闭输出流还是先关闭输入流都无影响(找不到反例证明有问题)
3.建议使用try-catch处理流关闭,内置IO流工具类都满足可直接使用,第三方流处理工具类要查看是否符合关闭先后顺序的问题