面试官:IO 操作必须要手动关闭吗?关闭流方法是否有顺序?

简介: 面试官:IO 操作必须要手动关闭吗?关闭流方法是否有顺序?

包装流的close方法是否会自动关闭被包装的流?


平时我们使用输入流和输出流一般都会使用buffer包装一下,直接看下面代码(这个代码运行正常,不会报错)


import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) throws IOException {
         FileOutputStream fileOutputStream = new FileOutputStream("c:\\a.txt");
         BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
         bufferedOutputStream.write("test write something".getBytes());
         bufferedOutputStream.flush();
         //从包装流中关闭流
         bufferedOutputStream.close();
    }
}


下面我们来研究下这段代码的bufferedOutputStream.close();方法是否调用了fileOutputStream.close();


先看BufferedOutputStream源代码:


public class BufferedOutputStream extends FilterOutputStream { ...

可以看到它继承FilterOutputStream,并且没有重写close方法,所以直接看FilterOutputStream的源代码:


public void close() throws IOException {
    try {
      flush();
    } catch (IOException ignored) {
    }
    out.close();
}


跟踪out(FilterOutputStream中):


protected OutputStream out;
  public FilterOutputStream(OutputStream out) {
        this.out = out;
  }


再看看BufferedOutputStream中:


public BufferedOutputStream(OutputStream out) {
    this(out, 8192);
}
public BufferedOutputStream(OutputStream out, int size) {
    super(out);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}


可以看到BufferedOutputStream调用super(out);,也就是说,out.close();调用的是通过BufferedOutputStream传入的被包装的流,这里就是FileOutputStream。


我们在看看其他类似的,比如BufferedWriter的源代码:


public void close() throws IOException {
    synchronized (lock) {
        if (out == null) {
            return;
        }
        try {
            flushBuffer();
        } finally {
            out.close();
            out = null;
            cb = null;
        }
    }
}


通过观察各种流的源代码,可得结论:包装的流都会自动调用被包装的流的关闭方法,无需自己调用。


关闭流方法是否有顺序?


由上面的结论,就会产生一个问题:如果手动关闭被包装流会怎么样,这个关闭流有顺序吗?而实际上我们习惯都是两个流都关闭的。


首先我们来做一个简单的实验,基于第一个问题的代码上增加手动增加关闭流的代码,那么就有两种顺序:


1.先关闭被包装流(正常没异常抛出)


import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) throws IOException {
         FileOutputStream fileOutputStream = new FileOutputStream("c:\\a.txt");
         BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
         bufferedOutputStream.write("test write something".getBytes());
         bufferedOutputStream.flush();
         fileOutputStream.close();//先关闭被包装流
         bufferedOutputStream.close();
    }
}


2.先关闭包装流(正常没异常抛出)


import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) throws IOException {
         FileOutputStream fileOutputStream = new FileOutputStream("c:\\a.txt");
         BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
         bufferedOutputStream.write("test write something".getBytes());
         bufferedOutputStream.flush();
         bufferedOutputStream.close();//先关闭包装流
         fileOutputStream.close();
    }
}


上述两种写法都没有问题,我们已经知道bufferedOutputStream.close();会自动调用fileOutputStream.close();方法,那么这个方法是怎么执行的呢?我们又看看FileOutputStream的源码:


public void close() throws IOException {
    synchronized (closeLock) {
        if (closed) {
            return;
        }
        closed = true;
    }
...


可以看出它采用同步锁,而且使用了关闭标记,如果已经关闭了则不会再次操作,所以多次调用不会出现问题。


如果没有看过参考文章,我可能就会断下结论,关闭流不需要考虑顺序


我们看下下面的代码(修改自参考文章):


import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
public class IOTest {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("c:\\a.txt");
        OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
        BufferedWriter bw = new BufferedWriter(osw);
        bw.write("java IO close test");
        // 从内带外顺序顺序会报异常
        fos.close();
        osw.close();
        bw.close();
    }
}


会抛出Stream closed的IO异常:


Exception in thread "main" java.io.IOException: Stream closed
    at sun.nio.cs.StreamEncoder.ensureOpen(StreamEncoder.java:45)
    at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:118)
    at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
    at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
    at java.io.BufferedWriter.close(BufferedWriter.java:264)
    at IOTest.main(IOTest.java:18)


而如果把bw.close();放在第一,其他顺序任意,即修改成下面两种:


bw.close();
osw.close();
fos.close();
bw.close();
fos.close();
osw.close();


都不会报错,这是为什么呢,我们立即看看BufferedWriter的close源码:


public void close() throws IOException {
    synchronized (lock) {
        if (out == null) {
            return;
        }
        try {
            flushBuffer();
        } finally {
            out.close();
            out = null;
            cb = null;
        }
    }
}


里面调用了flushBuffer()方法,也是抛异常中的错误方法:


void flushBuffer() throws IOException {
    synchronized (lock) {
        ensureOpen();
        if (nextChar == 0)
            return;
        out.write(cb, 0, nextChar);
        nextChar = 0;
    }
}


可以看到很大的一行


out.write(cb, 0, nextChar);


这行如果在流关闭后执行就会抛IO异常,有时候我们会写成:


fos.close();
fos = null;
osw.close();
osw = null;
bw.close();
bw = null;


这样也会抛异常,不过是由于flushBuffer()中ensureOpen()抛的,可从源码中看出:


private void ensureOpen() throws IOException {
    if (out == null)
        throw new IOException("Stream closed");
}
void flushBuffer() throws IOException {
    synchronized (lock) {
        ensureOpen();
        if (nextChar == 0)
            return;
        out.write(cb, 0, nextChar);
        nextChar = 0;
    }
}


如何防止这种情况?


直接写下面这种形式就可以:


bw.close();
bw = null;


结论:一个流上的close方法可以多次调用,理论上关闭流不需要考虑顺序,但有时候关闭方法中调用了write等方法时会抛异常。


由上述的两个结论可以得出下面的建议:


关闭流只需要关闭最外层的包装流,其他流会自动调用关闭,这样可以保证不会抛异常。如:


bw.close();
//下面三个无顺序
osw = null;
fos = null;
bw = null;


注意的是,有些方法中close方法除了调用被包装流的close方法外还会把包装流置为null,方便JVM回收。bw.close()中的:


public void close() throws IOException {
        synchronized (lock) {
            if (out == null) {
                return;
            }
            try {
                flushBuffer();
            } finally {
                out.close();
                out = null;
                cb = null;
            }
        }
    }


finally中就有把out置为null的代码,所以有时候不需要自己手动置为null。(个人建议还是写一下,不差多少执行时间)


相关文章
|
存储 Linux API
Linux应用开发基础知识——文件IO操作(三)
Linux应用开发基础知识——文件IO操作(三)
222 2
Linux应用开发基础知识——文件IO操作(三)
|
存储 网络协议 Java
程序员的23大IO&NIO面试问题及答案
程序员的23大IO&NIO面试问题及答案
|
安全 网络安全 数据安全/隐私保护
CocosCreator 面试题(十四)Cocos Creator WebSocket 、Socket.IO分别是什么?
CocosCreator 面试题(十四)Cocos Creator WebSocket 、Socket.IO分别是什么?
909 0
|
数据采集 异构计算
LabVIEW编程LabVIEW开发高级数据采集技术 操作数字IO 例程与相关资料
LabVIEW编程LabVIEW开发高级数据采集技术 操作数字IO 例程与相关资料
255 22
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
消息中间件 NoSQL Java
面试官:谈谈你对IO多路复用的理解?
面试官:谈谈你对IO多路复用的理解?
216 0
面试官:谈谈你对IO多路复用的理解?
|
缓存 NoSQL Redis
redis管道操作(节省网络IO开销)
pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败,后面命令不会有影响,继续执行。
182 1
|
消息中间件 关系型数据库 Kafka
实时计算 Flink版操作报错之在执行任务时遇到了一个IO错误,具体表现为无法从本地主机(localhost)下载文件,该怎么解决
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
监控 Java
Java一分钟之-NIO:非阻塞IO操作
【5月更文挑战第14天】Java的NIO(New IO)解决了传统BIO在高并发下的低效问题,通过非阻塞方式提高性能。NIO涉及复杂的选择器和缓冲区管理,易出现线程、内存和中断处理的误区。要避免这些问题,可以使用如Netty的NIO库,谨慎设计并发策略,并建立标准异常处理。示例展示了简单NIO服务器,接收连接并发送欢迎消息。理解NIO工作原理和最佳实践,有助于构建高效网络应用。
321 2