天天看點

Java新特性-try-with-resource

在我們的項目中,所有被打開的系統資源,比如流、檔案或者Socket連接配接等,都需要被開發者手動關閉,否則随着程式的不斷運作,資源洩露将會累積成重大的生産事故。

傳統的try

文法

try{
    //進行可能出現異常的操作
}catch(捕獲的異常名稱){
    //進行異常處理
}finally{
    //關閉資源操作      

執行個體代碼如下:

public static void try1() {
        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")));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bin != null) {
                try {
                    bin.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bout != null) {// 確定即使BufferedInputStream在執行close()方法時發生異常,也執行BufferedOutputStream的close()方法
                try {
                    bout.close();
                } catch      
  • 使用finally塊來關閉實體資源,保證關閉操作總是會被執行。
  • 關閉每個資源之前首先保證引用該資源的引用變量不為null。
  • 為每一個實體資源使用單獨的try…catch塊來關閉資源,保證關閉資源時引發的異常不會影響其他資源的關閉。
  • 以上方式導緻finally塊代碼十分臃腫,關閉資源的代碼比業務代碼還要多,程式的可讀性降低。

java7增強try

為了解決以上傳統方式的問題, Java7新增了自動關閉資源的try語句。它允許在try關鍵字後緊跟一對圓括号,裡面可以聲明、初始化一個或多個資源,此處的資源指的是那些必須在程式結束時顯示關閉的資源(資料庫連接配接、網絡連接配接等),try語句會在該語句結束時自動關閉這些資源。

文法

try(初始化可能出現異常的操作){
    //進行操作
}ctach(捕獲的異常名稱){
    //進行異常操作      

打開一個資源

try(
    打開一個資源的語句;//末尾可以加";",也可以不加
){
    //進行操作      

打開多個資源

try(
    打開一個資源的語句;
    打開一個資源的語句;
    打開一個資源的語句;//多個表達式末尾加";",最後一個表達式末尾可以不加";"
){
    //進行操作
}      

改寫以上代碼如下:

//捕獲異常
    public static void try2() {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
                BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));) {
            // 執行完try中的語句過後,資源自動關閉
        } catch      

或者

//抛出異常
    public static void try2() throws FileNotFoundException, IOException {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
                BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));) {
            // 執行完try中的語句過後,資源自動關閉      

自動關閉資源的try語句相當于包含了隐式的finally塊(用于關閉資源),是以這個try語句可以既沒有catch塊,也沒有finally塊。

動手測試

為了能夠配合try-with-resource,資源必須實作AutoCloseable接口。且該接口的實作類需要重寫close方法:

package tryTest;

/**
 * 
 * @ClassName: Connection
 * @Description: 建立執行個體,實作AutoCloseable接口
 * @author cheng
 * @date
public class Connection implements AutoCloseable

    public void sendData() throws Exception {
        System.out.println("正在發送資料........");
    }

    /**
     * 重寫close方法
     */
    @Override
    public void close() throws Exception {
        System.out.println("正在自動關閉連接配接........");
    }

}      

測試

public static void try3() {
        try (Connection conn = new Connection();) {
            conn.sendData();
        } catch      

運作結果:

正在發送資料........
正在自動關閉連接配接........      

通過結果我們可以看到,close方法被自動調用了。

異常屏蔽處理

修改Connection,模拟抛出異常

package tryTest;

/**
 * 
 * @ClassName: Connection
 * @Description: 建立執行個體,實作AutoCloseable接口
 * @author cheng
 * @date
public class Connection implements AutoCloseable

    public void sendData() throws Exception {
        System.out.println("正在發送資料........");
        throw new Exception("模拟發送時發生了異常.....");
    }

    /**
     * 重寫close方法
     */
    @Override
    public void close() throws Exception {
        System.out.println("正在自動關閉連接配接........");
        throw new Exception("模拟關閉時發生了異常.....");
    }

}      

使用傳統的try,手動關閉異常

public static void try5() throws Exception {
        Connection conn = null;
        try {
            conn = new Connection();
            conn.sendData();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }      

調用:

public static void main(String[] args) {
        try {
            try5();
        } catch      

運作結果:

正在發送資料........
正在自動關閉連接配接........
java.lang.Exception: 模拟關閉時發生了異常.....
    at tryTest.Connection.close(Connection.java:23)
    at tryTest.TryTest.try5(TryTest.java:40)
    at tryTest.TryTest.main(TryTest.java:26)      

好的,問題來了,由于我們一次隻能抛出一個異常,是以在最上層看到的是最後一個抛出的異常——也就是close方法抛出的模拟異常,而sendData抛出的Exception被忽略了。這就是所謂的異常屏蔽。

由于異常資訊的丢失,異常屏蔽可能會導緻某些bug變得極其難以發現,程式員們不得不加班加點地找bug.

為了解決這個問題,從Java 1.7開始,大佬們為Throwable類新增了addSuppressed方法,支援将一個異常附加到另一個異常身上,進而避免異常屏蔽。那麼被屏蔽的異常資訊會通過怎樣的格式輸出呢?我們再運作一遍剛才用try-with-resource包裹的main方法:

public static void try3() {
        try (Connection conn = new Connection();) {
            conn.sendData();
        } catch      

運作結果:

正在發送資料........
正在自動關閉連接配接........
java.lang.Exception: 模拟發送時發生了異常.....
    at tryTest.Connection.sendData(Connection.java:14)
    at tryTest.TryTest.try3(TryTest.java:59)
    at tryTest.TryTest.main(TryTest.java:23)
    Suppressed: java.lang.Exception: 模拟關閉時發生了異常.....
        at tryTest.Connection.close(Connection.java:23)
        at tryTest.TryTest.try3(TryTest.java:60)
        ... 1      

可以看到,異常資訊中多了一個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      

在上述代碼中,我們從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      

由于編譯器會自動生成fout.close()的代碼,這樣肯定能夠保證真正的流被關閉。

總結:同時打開多個資源時,單獨聲明每個資源!

注意

  • 被自動關閉的資源必須實作Closeable或AutoCloseable接口。(Closeable是AutoCloseable的子接口,Closeable接口裡的close()方法聲明抛出了IOException,;AutoCloseable接口裡的close()方法聲明抛出了Exception)
  • 被關閉的資源必須放在try語句後的圓括号中聲明、初始化。如果程式有需要自動關閉資源的try語句後可以帶多個catch塊和一個finally塊。
  • Java7幾乎把所有的“資源類”(包括檔案IO的各種類,JDBC程式設計的Connection、Statement等接口……)進行了改寫,改寫後的資源類都實作了AutoCloseable或Closeable接口

AutoCloseable 源碼:

package java.lang;

public interface AutoCloseable

    void close() throws      

Closeable源碼:

package java.lang;

public interface Closeable implements AutoCloseable

    void close() throws      

若我們在try中打開的資源類沒有實作AutoCloseable接口或者Closeable接口,則會報以下錯誤:

The resource type Connection does not      

捕獲多個異常

當我們操作一個對象的時候,它有時候會抛出多個異常,例如:

public static void try6() {
        try {
            Thread.sleep(20000);
            FileInputStream fis = new FileInputStream("/a/b,txt");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch      

這樣寫起來很繁瑣,java7優化後代碼如下:

public static void try7() {
        try {
            Thread.sleep(20000);
            FileInputStream fis = new FileInputStream("/a/b,txt");
        } catch      

并且catch語句後面的異常參數是final的,不可以再修改

處理反射異常

java7之前使用反射時會有許多需要處理的異常,代碼如下:

public static void try7() {
        Object object = null;
        Class<?> clazz;
        try {
            clazz = Class.forName("tryTest.Connection");
            clazz.getMethods()[0].invoke(object);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch      
public static void try7() {
        Object object = null;
        Class<?> clazz;
        try {
            clazz = Class.forName("tryTest.Connection");
            clazz.getMethods()[0].invoke(object);
        } catch      
public static void try7() {
        Object object = null;
        Class<?> clazz;
        try {
            clazz = Class.forName("tryTest.Connection");
            clazz.getMethods()[0].invoke(object);
        } catch