面試官:IO 操作必須要手動關閉嗎?關閉流方法是否有順序?

前幾天看了一篇文章,自己動手試了下,發現有些不一樣結論,作博客記錄下,本文主要研究兩個問題:

  • 包裝流的close方法是否會自動關閉被包裝的流?
  • 關閉流方法是否有順序?

包裝流的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。(個人建議還是寫一下,不差多少執行時間)

來源:blog.csdn.net/maxwell_nc/article/details/49151005

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這纔是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章