Java-IO 流的Close方法

一、在Java中爲何要關閉流

GC運行的時間點是不確定的(因爲是一條單獨存在的線程),所以很多時候你不能直接控制什麼時候發生GC。這個帶來的問題有兩點,一個是有時候你的內存不足需要立刻回收而GC並不會立刻運行;另外一個是因爲GC運行期間會佔用大量系統資源所以某些情況下你會希望把它推後,或者乾脆關掉以便根據性能需求在合式的時候手動執行。

另外,GC只能回收內存。至於各種stream之類,他們下邊一般還開啓了各種其他的系統資源,比如文件,比如輸入輸出設備(鍵盤/屏幕等),等等。而這些設備第一是不能自動關閉(因爲誰知道你程序要用它到什麼時候啊),另一個系統內數量有限(比如鍵盤/屏幕同一時間只有一個)。最後,文件和數據庫連接之類的東西還存在讀寫鎖定的問題。這些都導致用戶必須手動處理這些資源的開啓和關閉。

這年頭自動擋汽車都那麼好了還不是有那麼多人喜歡手擋,一樣的。

流不關資源佔着內存,你一個小的程序感覺不出來,要是好多流都不關,就會導致死機,內存泄流!建議培養良好的編碼意識,一個小的程序也要吧流關了。

二、TryWithResources

這個不妨直接來看Oracle公司所提供的JavaDoc好了,畢竟可以這麼說,這只是從Java SE 7開始提供的一個語法糖。 https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

官方描述:

The try-with-resources statement is a try statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.

解釋一下:

使用try語句來聲明一個或多個資源,這裏的資源都是在程序執行完畢之後必須要被關閉的對象。TryWithResources聲明確保了每一個資源最終都會在程序運行的最後被關閉。但我們要求,每一個資源對象必須實現java.lang.AutoCloseable包括實現了java.io.Closeable的對象都可以被作爲資源對象。

如果在Java SE 7 之前,我們關閉一個流對象,需要如下的寫法:

static String readFirstLineFromFileWithFinallyBlock(String path)
                                                     throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    }catch(//...){
        //...
    }finally {
        if (br != null) br.close();
    }
}

將close()方法置於finally語句塊中是一個常見的做法。

使用Java SE 7 之後,使用TryWithResources,我們就可以更優雅地關閉流對象了:

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
                   new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }catch(//...){
        //.....
    }
}

try-with-resource 結構的用法即是,FileInputStream 類型變量就在try關鍵字後面的括號中聲明,而finally{}的處理實際上是一樣的。

一個還看不出此語法糖的優勢,比如有多個流要進行關閉,在傳統方法中,一個流關閉就應該對應一個try-catch語句,例子如下:

	try {
	        FileOutputStream fos = new FileOutputStream("d:\\a.txt");
	        OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
	        BufferedWriter bw = new BufferedWriter(osw);
	        bw.write("java IO close test");
	
	        // 從外到內順序關閉ok
	        if (bw != null) {
	           bw.close();
	        }
	        if (osw!= null) {
	            osw.close();
	        } if (fos!= null) {
	            fos.close();
	        }
		}catch (Exception e){
	    }
  

我們假設一個bw流出現了異常,那麼直接被捕獲了異常,那麼後面兩個流的close方法沒能成功被調用,那麼就會導致流沒有被關閉,所以要寫成以下寫法:

finally {
	try{
		if(osw!= null){
		  osw.close();
		}
		}catch(Exception e){
		}
		
		try{
		if(fos!= null){
		  fos.close();
		}
		}catch(Exception e){
		}
}

每一次關閉流我們都單獨進行一次try,而且需要寫在finally中保證異常了也要執行,不要嫌棄代碼繁瑣因爲這是必須的內容。

然而在使用了try-with-resources(注意:resources使用的是複數,說明可以一次聲明多個資源)之後,代碼簡單多了:

try(FileOutputStream fos = new FileOutputStream("d:\\a.txt");
	OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
	BufferedWriter bw = new BufferedWriter(osw))
}catch (Exception e){
	    }

節約了很多不少額外的相似代碼,這也是Java語法糖帶給開發者的便利之處,這也是我們應當在Java中使用的流關閉方式。在實際開發中,還是儘量使用新特性吧!

三、包裝流的關閉

引用於:Java IO包裝流如何關閉?
問題:

  1. JAVA的IO流使用了裝飾模式,關閉最外面的流的時候會自動調用被包裝的流的close()方嗎?

  2. 如果按順序關閉流,是從內層流到外層流關閉還是從外層到內存關閉?

問題 1 的解釋:

  FileInputStream is = new FileInputStream(".");   
  BufferedInputStream bis = new BufferedInputStream(is);  
  bis.close();

從設計模式上看:
java.io.BufferedInputStream是java.io.InputStream的裝飾類。
BufferedInputStream裝飾一個 InputStream 使之具有緩衝功能,is要關閉只需要調用最終被裝飾出的對象的 close()方法即可,因爲它最終會調用真正數據源對象的 close()方法。

因此,可以只調用外層流的close方法關閉其裝飾的內層流,驗證例子:(我對上述應用博文做了一些改進):

主要思路是:繼承後重寫close方法,提供一個額外的判斷布爾值,來告訴我們內層流對象的close方法是否因爲外層流對象調用close方法而調用:

import java.io.*;

/**
 * @author Fisherman
 */
public class Test4 {
    public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("d:\\a.txt");
        OutputStreamWriter_my osw = new OutputStreamWriter_my(fos, "UTF-8");
        BufferedWriter bw = new BufferedWriter(osw);
        bw.write("java IO close test");

        bw.close();

        if (osw.ifClosed) {
            System.out.println("外層流導致了內層流的關閉");
        }

    }
}

class OutputStreamWriter_my extends OutputStreamWriter {
    public OutputStreamWriter_my(OutputStream out, String charsetName) throws UnsupportedEncodingException {
        super(out, charsetName);
    }

    public boolean ifClosed = false;

    @Override
    public void close() throws IOException {

        super.close();
        ifClosed = true;
    }
}

問題 2 的解釋:如果不想使用(1)方式關閉流,可以逐個關閉流(可能大家比較習慣吧):

public class Test4 {
    public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("d:\\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();

    }
}
報出異常:
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:265)
	at com.fisherman.learnIO.Test4.main(Test4.java:19)

如果改爲從外到內的流關閉順序:

 public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("d:\\a.txt");
        OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
        BufferedWriter bw = new BufferedWriter(osw);
        bw.write("java IO close test");

        // 從外到內順序關閉ok
        bw.close();
        osw.close();
        fos.close();
    }

程序正確執行。

一般情況下是:先打開的後關閉,後打開的先關閉

另一種情況:看依賴關係,如果流a依賴流b,應該先關閉流a,再關閉流b

例如處理流a依賴節點流b,應該先關閉處理流a,再關閉節點流b

當然完全可以只關閉處理流,不用關閉節點流。處理流關閉的時候,會調用其處理的節點流的關閉方法

如果將節點流關閉以後再關閉處理流,會拋出IO異常;

四、如何正確方式關閉流

使用try-with-resources語句,或者對每個流對象建立一套try-with語句塊來進行流的關閉。

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