Java基礎-try-with-resource語法糖

原文地址:juejin.im/entry/57f73…java

背景

衆所周知,全部被打開的系統資源,好比流、文件或者Socket鏈接等,都須要被開發者手動關閉,不然隨着程序的不斷運行,資源泄露將會累積成重大的生產事故。程序員

在Java的江湖中,存在着一種名爲finally的功夫,它能夠保證當你習武走火入魔之時,還能夠作一些自救的操做。在遠古時代,處理資源關閉的代碼一般寫在finally塊中。然而,若是你同時打開了多個資源,那麼將會出現噩夢般的場景:shell

public class Demo {
    public static void main(String[] args) {
        BufferedInputStream bin = null;
        BufferedOutputStream bout = null;
        try {
            bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
            bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (bin != null) {
                try {
                    bin.close();
                }
                catch (IOException e) {
                   throw e;
                }
                finally {
                    if (bout != null) {
                        try {
                            bout.close();
                        }
                        catch (IOException e) {
                            throw e;
                        }
                    }
                }
            }
        }
    }
}
複製代碼

Oh My God!!!關閉資源的代碼居然比業務代碼還要多!!!這是由於,咱們不只須要關閉BufferedInputStream,還須要保證若是關閉BufferedInputStream時出現了異常, BufferedOutputStream也要能被正確地關閉。因此咱們不得不借助finally中嵌套finally大法。能夠想到,打開的資源越多,finally中嵌套的將會越深!!!ide

Java 1.7中新增的try-with-resource語法糖來打開資源,而無需碼農們本身書寫資源來關閉代碼。不再用擔憂我把手寫斷掉了!咱們用try-with-resource來改寫剛纔的例子:spa

public class TryWithResource {
    public static void main(String[] args) {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
             BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

動手實踐

爲了可以配合try-with-resource,資源必須實現AutoClosable接口。該接口的實現類須要重寫close方法:code

public class Connection implements AutoCloseable {
    public void sendData() {
        System.out.println("正在發送數據");
    }
    @Override
    public void close() throws Exception {
        System.out.println("正在關閉鏈接");
    }
}
複製代碼

調用類:接口

public class TryWithResource {
    public static void main(String[] args) {
        try (Connection conn = new Connection()) {
            conn.sendData();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

運行後輸出結果:資源

正在發送數據
正在關閉鏈接
複製代碼

原理

那麼這個是怎麼作到的呢?我相信聰明的大家必定已經猜到了,其實,這一切都是編譯器大神搞的鬼。咱們反編譯剛纔例子的class文件:開發

package com.codersm.trywithresource;

public class TryWithResource {
    public TryWithResource() {
    }

    public static void main(String[] args) {
        try {
            Connection conn = new Connection();
            Throwable var2 = null;

            try {
                conn.sendData();
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                if (conn != null) {
                    if (var2 != null) {
                        try {
                            conn.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        conn.close();
                    }
                }

            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }

    }
}
複製代碼

看到沒,在第15~27行,編譯器自動幫咱們生成了finally塊,而且在裏面調用了資源的close方法,因此例子中的close方法會在運行的時候被執行。get

異常屏蔽

細心的大家確定又發現了,剛纔反編譯的代碼(第21行)比遠古時代寫的代碼多了一個addSuppressed。爲了瞭解這段代碼的用意,咱們稍微修改一下剛纔的例子:咱們將剛纔的代碼改回遠古時代手動關閉異常的方式,而且在sendDataclose方法中拋出異常:

public class Connection implements AutoCloseable {
    public void sendData() throws Exception {
        throw new Exception("send data");
    }
    @Override
    public void close() throws Exception {
        throw new MyException("close");
    }
}
複製代碼

修改main方法:

public class TryWithResource {
    public static void main(String[] args) {
        try {
            test();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static void test() throws Exception {
        Connection conn = null;
        try {
            conn = new Connection();
            conn.sendData();
        }
        finally {
            if (conn != null) {
                conn.close();
            }
        }
    }
}
複製代碼

運行以後咱們發現:

basic.exception.MyException: close
	at basic.exception.Connection.close(Connection.java:10)
	at basic.exception.TryWithResource.test(TryWithResource.java:82)
	at basic.exception.TryWithResource.main(TryWithResource.java:7)
	......
複製代碼

好的,問題來了,因爲咱們一次只能拋出一個異常,因此在最上層看到的是最後一個拋出的異常——也就是close方法拋出的MyException,而sendData拋出的Exception被忽略了。這就是所謂的異常屏蔽。因爲異常信息的丟失,異常屏蔽可能會致使某些bug變得極其難以發現,程序員們不得不加班加點地找bug,如此毒瘤,怎能不除!幸虧,爲了解決這個問題,從Java 1.7開始,大佬們爲Throwable類新增了addSuppressed方法,支持將一個異常附加到另外一個異常身上,從而避免異常屏蔽。那麼被屏蔽的異常信息會經過怎樣的格式輸出呢?咱們再運行一遍剛纔用try-with-resource包裹的main方法:

java.lang.Exception: send data

	at basic.exception.Connection.sendData(Connection.java:5)
	at basic.exception.TryWithResource.main(TryWithResource.java:14)
	......
	Suppressed: basic.exception.MyException: close
		at basic.exception.Connection.close(Connection.java:10)
		at basic.exception.TryWithResource.main(TryWithResource.java:15)
		... 5 more
複製代碼

能夠看到,異常信息中多了一個Suppressed的提示,告訴咱們這個異常其實由兩個異常組成,MyException是被Suppressed的異常。可喜可賀!

注意事項

在使用try-with-resource的過程當中,必定須要瞭解資源的close方法內部的實現邏輯。不然仍是可能會致使資源泄露。

舉個例子,在Java BIO中採用了大量的裝飾器模式。當調用裝飾器的close方法時,本質上是調用了裝飾器內部包裹的流的close方法。好比:

public class TryWithResource {
    public static void main(String[] args) {
        try (FileInputStream fin = new FileInputStream(new File("input.txt"));
                GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File("out.txt")))) {
            byte[] buffer = new byte[4096];
            int read;
            while ((read = fin.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

在上述代碼中,咱們從FileInputStream中讀取字節,而且寫入到GZIPOutputStream中。GZIPOutputStream其實是FileOutputStream的裝飾器。因爲try-with-resource的特性,實際編譯以後的代碼會在後面帶上finally代碼塊,而且在裏面調用fin.close()方法和out.close()方法。咱們再來看GZIPOutputStream類的close方法:

public void close() throws IOException {
    if (!closed) {
        finish();
        if (usesDefaultDeflater)
            def.end();
        out.close();
        closed = true;
    }
}
複製代碼

咱們能夠看到,out變量實際上表明的是被裝飾的FileOutputStream類。在調用out變量的close方法以前,GZIPOutputStream還作了finish操做,該操做還會繼續往FileOutputStream中寫壓縮信息,此時若是出現異常,則會out.close()方法被略過,然而這個纔是最底層的資源關閉方法。正確的作法是應該在try-with-resource中單獨聲明最底層的資源,保證對應的close方法必定可以被調用。在剛纔的例子中,咱們須要單獨聲明每一個FileInputStream以及FileOutputStream

public class TryWithResource {
    public static void main(String[] args) {
        try (FileInputStream fin = new FileInputStream(new File("input.txt"));
                FileOutputStream fout = new FileOutputStream(new File("out.txt"));
                GZIPOutputStream out = new GZIPOutputStream(fout)) {
            byte[] buffer = new byte[4096];
            int read;
            while ((read = fin.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

因爲編譯器會自動生成fout.close()的代碼,這樣確定可以保證真正的流被關閉。

相關文章
相關標籤/搜索