Java的流操作在实际应用中使用的很多,但是流的关闭顺序到底有没有要求,关闭流的顺序和节点流与处理流有什么关系,输出流和输入流又有什么区别,然后就是关闭流可以通过哪几种方式。这篇文章将会和大家讨论一下。
1. 节点流和处理流
先来了解一下节点流和处理流流的概念以及对应常用的类。
1.1 节点流
概念:可以从或向一个特定的地方(节点)读写数据。
常用节点流:
- 文 件:FileInputStream、FileOutputStrean、FileReader、FileWriter 文件进行处理的节点流。
- 字符串:StringReader、StringWriter 对字符串进行处理的节点流。
- 数 组:ByteArrayInputStream、ByteArrayOutputStreamCharArrayReader、CharArrayWriter 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)。
- 管 道:PipedInputStream、PipedOutputStream、PipedReaderPipedWriter对管道进行处理的节点流。
1.2 处理流
概念:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。
常用处理流:
缓冲流:BufferedInputStrean、BufferedOutputStrea、 BufferedReader、BufferedWriter增加缓冲功能,避免频繁读写硬盘。
转换流:InputStreamReader、OutputStreamReader实现字节流和字符流之间的转换。
数据流:DataInputStream、DataOutputStream 等-提供将基础数据类型写入到文件中,或者读取出来。
2. 流的关闭顺序
流的关闭到底有没有顺序要求呢,在输出流和输入流中,到底有什么区别呢?
2.1 输入流关闭顺序
看示例代码:
private static void testInputStream() {
try {
File file = new File(Demo.class.getClassLoader()
.getResource("application.properties").getFile());
InputStream in = new FileInputStream(file);
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
int read = reader.read();
in.close();//关闭节点流
reader.close();//关闭处理流
} catch (Exception e) {
System.out.println("异常:" + e);
}
}
这样关闭过程是没有异常出现的,能够正常关闭。如果将关闭节点流和处理流两个过程调换位置,会发现也是正常的关闭,没有异常出现。
2.2 输出流关闭顺序
看示例代码:
private static void testOutputStream() {
try {
OutputStream out = new FileOutputStream("/Volumes/joker/xxx.md");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(new char[123]);
writer.close();//关闭处理流
out.close();//关闭节点流
} catch (Exception e) {
System.out.println("异常:" + e);
}
}
先关闭处理流,然后再关闭节点流,关闭过程正常。接下来就是将关闭顺序调换,这个时候会抛出以下异常:
异常:java.io.IOException: Stream Closed
为什么会有这个异常,下面的源码中来说。
2.3 输入流关闭(close)源码分析
在先看关闭节点流的close方法:
public void close() throws IOException {
synchronized (closeLock) {
if (closed) { //首先判断流是否已经被关闭
return;
}
closed = true;//设置为关闭
}
if (channel != null) {
channel.close();
}
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}
源码很简洁,首先判断流是否关闭的标识符closed,然后再确定是否要关闭。还有要注意的是这里需要加锁,很好理解,不解释。
然后看关闭处理流的close方法:
public void close() throws IOException {
synchronized (lock) {
if (in == null)//判断流是否已经关闭
return;
try {
in.close();//关闭流
} finally {
in = null;//help GC
cb = null;
}
}
}
这里需要注意的是in是什么,从哪里来,在创建处理流对象的时候会将节点流作为参数传入到处理流中,这个时候就会将传入的节点流赋值给in。可以看一下下面的源码:
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
public BufferedReader(Reader in, int sz) {
super(in);//调用父类的构造方法
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.in = in; //将in赋值给全局变量
cb = new char[sz];
nextChar = nChars = 0;
}
至此就很清楚了,在构建处理流的时候将节点流传入,然后在关闭的时候,实际上调用的是节点流的close方法,在close的时候,都会去判断当前流是否已经被关闭,如果被关闭就不会重复的进行关闭的操作。加上这个判断自然就不会有异常的出现。
2.4 输出流关闭(close)源码分析
其实这里和上面的实现思路是一样的,但是在输出流的处理流中存在一个flush操作,这个时候会对流是否关闭做判断:
public void close() throws IOException {
synchronized (lock) {
if (out == null) {
return;
}
try (Writer w = out) {
flushBuffer();//调用刷出缓存操作
} finally {
out = null;
cb = null;
}
}
}
void flushBuffer() throws IOException {
synchronized (lock) {
ensureOpen();//判断流的可用性
if (nextChar == 0)
return;
out.write(cb, 0, nextChar);
nextChar = 0;
}
}
private void ensureOpen() throws IOException {
if (out == null) //判断流是否为空
throw new IOException("Stream closed");
}
这里就能看的出来,当先关闭节点流后,会导致out为null,此时就会导致出现Stream closed
异常的出现。
2.4 小结
流使用的设计模式的装饰器模式,当执行close的方法的时候,会依次向上调用,直到最内层的流操作对象,并将流关闭,而实际上外层的装饰器本身不会缓存流数据,也不会产生流的占用资源的问题。听着很抽象,可以举个栗子。
核心持有权利的是皇上,外面有层层的士兵包围保护着,并将外界的信息通过士兵层层传入,如果士兵死了,只要皇上还在,就不会释放这个权利,但是如果直接穿过所有的士兵,直接取了皇上的首级,这个权利才会真正的被释放出来。
流关闭顺序:
- 当是输入流的时候,节点流和处理流的关闭顺序没有硬性的要求,如果偷懒,只需关闭处理流即可。
- 当时输出流的时候,需要先关闭处理流,在关闭节点流,当然这里节点流也可以不用手动关闭,因为处理流已经帮助做了关闭的动作。
3. 流的关闭姿势
3.1 直接关闭
直接关闭,这种方式是最简便的,上面的代码都是这样操作的,也是最不安全。如果启动流操作,在操作的过程中出现了异常,导致后续的关闭代码没有执行,流就无法释放。下面提供了两种更为安全的关闭流方法。
3.2 finally代码块中关闭
在finally代码块中关闭流是最常用的方式,示例代码:
public static void closeStream1() {
File file = new File(Demo.class.getClassLoader()
.getResource("application.properties").getFile());
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(file);
out = new FileOutputStream("/Volumes/joker/xxx.md");
} catch (Exception e) {
System.out.println("异常:" + e);
}finally {
try {
if(in != null) in.close();
if(out != null) out.close();
}catch (Exception e){
System.out.println("关闭流失败");
}
}
}
finally代码块肯定会被执行,流肯定会被关闭。但是事实真的如此吗,存在特例吗?接下来的篇幅解释。
3.3 try代码块自动关闭
JDK7以后,JDK提供了一种更为方便的关闭流方式:
public static void closeStream2() {
File file = new File(Demo.class.getClassLoader()
.getResource("application.properties").getFile());
try (InputStream in = new FileInputStream(file);
OutputStream out = new FileOutputStream("/Volumes/joker/xxx.md")){
int read = in.read();
out.write(new byte[11]);
} catch (Exception e) {
System.out.println("异常:" + e);
}
}
在try的后面添加了一个()
,在创建流对象的时候,可以在此位置实现,当try块执行结束或者try块出现异常的时候,会自动去关闭括号中创建的流对象。代替了finally代码块,让代码更为的简洁。这种方法是不是也是万能的呢?
3.4 关闭流的注意问题
在线程里面有一种线程被称为守护线程。它是随主线程的死亡而死亡。需要注意的是,当主线程执行结束死亡时,守护线程也会立即死亡,即使有finally代码块,也不会被执行。
因此在上面关闭流的时候,使用finally代码块关闭流在守护线程中可能会失效。
示例代码:
public static void test(){
Thread thread = new Thread(() -> closeStream() );
thread.setDaemon(true);
thread.start();
System.out.println("主线程执行结束……");
}
public static void closeStream() {
File file = new File(Demo.class.getClassLoader()
.getResource("application.properties").getFile());
InputStream in = null;
try {
in = new FileInputStream(file);
Thread.sleep(1000);//休眠
} catch (Exception e) {
System.out.println("异常:" + e);
} finally {
System.out.println("守护线程关闭流……");//标记
try {
if (in != null) in.close();
} catch (Exception e) {
System.out.println("关闭流失败");
}
}
}
让守护线程休眠1秒,当执行test的后,输出的结果只有主线程执行结束,而守护线程的finally中没有任何输出。
如果是使用JDK7中新的关闭方式呢?示例代码:
public static void test(){
Thread thread = new Thread(() -> closeStream() );
thread.setDaemon(true);
thread.start();
System.out.println("主线程执行结束……");
}
public static void closeStream() {
File file = new File(Demo.class.getClassLoader()
.getResource("application.properties").getFile());
try (InputStream in = new FileInputStream(file)) {
Thread.sleep(1000);//休眠
int read = in.read();
System.out.println(read);
} catch (Exception e) {
System.out.println("异常:" + e);
}
}
这个不能直接通过打印信息看出来,不过可以进入源码,在close方法上加上断点,你会发现close方法是被调用的,所以可以说明,当使用JDK7的方式关闭流,在守护线程中也是生效的。
可以得出结论在关闭流的时候最优的方案是通过JDK7中心的关闭流方式,能避免finally失效没有执行的问题。
4. 总结
整个篇幅说了节点流和处理流的区别,然后根据输出流和输入流讨论流的关闭顺序问题,然后就是流不同的关闭姿势说明,最后比较流不同关闭姿势的优缺点。每个点说明后都有总结的内容,最终的总结就不多说明了。希望这篇文章能对你使用流的时候有帮助。