Java输入和输出流关闭的顺序和关闭的姿势对比理解


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. 总结

整个篇幅说了节点流和处理流的区别,然后根据输出流和输入流讨论流的关闭顺序问题,然后就是流不同的关闭姿势说明,最后比较流不同关闭姿势的优缺点。每个点说明后都有总结的内容,最终的总结就不多说明了。希望这篇文章能对你使用流的时候有帮助。

5. 参考文章


文章作者: 程序猿洞晓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 程序猿洞晓 !
评论
 上一篇
发送邮件的JavaMail和Spring提供的MailSender,以及比较 发送邮件的JavaMail和Spring提供的MailSender,以及比较
发邮件,项目的必备功能之一,如果一个稍微模块化一点的公司,一般会单独出来一个项目专用来做公司的发送信息的功能,当然这个发送信息中不止包含发邮件,还会有短信、APP push等。这篇聊聊推送邮件。在以前的开发中,公司用Java mail的比较多,由自己来写邮件的组装和发送功能,但是Java mail使用操作比较繁杂,后来渐渐……
2018-10-28
下一篇 
  目录