一、在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包裝流如何關閉?
問題:
-
JAVA的IO流使用了裝飾模式,關閉最外面的流的時候會自動調用被包裝的流的close()方嗎?
-
如果按順序關閉流,是從內層流到外層流關閉還是從外層到內存關閉?
問題 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語句塊來進行流的關閉。