1. 程式人生 > >技術大佬:我去,你竟然還在用 try–catch-finally

技術大佬:我去,你竟然還在用 try–catch-finally

二哥,你之前那篇 我去 switch 的文章也特麼太有趣了,讀完後意猶未盡啊,要不要再寫一篇啊?雖然用的是 Java 13 的語法,對舊版本不太友好。但誰能保證 Java 不會再來一次重大更新呢,就像 Java 8 那樣,活生生地把 Java 6 拍死在了沙灘上。Java 8 是香,但早晚要升級,我挺你,二哥,別在乎那些反對的聲音。

這是讀者 Alice 上週特意給我發來的資訊,真令我動容。的確,上次的“我去”閱讀量槓槓的,幾個大號都轉載了,包括 CSDN,次條當天都 1.5 萬閱讀。但比如“還以為你有什麼新特技,沒想到用的是 Java 13”這類批評的聲音也不在少數。

不過我的心一直很大。從我寫第一篇文章至今,被噴的次數就好像頭頂上茂密的髮量一樣,數也數不清。所以我決定再接再厲,帶來新的一篇“我去”。

這次不用遠端 review 了,因為我們公司也復工了。這次 review 的程式碼仍然是小王的,他編寫的大部分程式碼都很漂亮,嚴謹的同時註釋也很到位,這令我非常滿意。但當我看到他沒用 try-with-resources 時,還是忍不住破口大罵:“我擦,小王,你丫的竟然還在用 try–catch-finally!”

來看看小王寫的程式碼吧。

public class Trycatchfinally {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("/牛逼.txt"));
            String str = null;
            while ((str =br.readLine()) != null) {
                System.out.println(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

咦,感覺這段程式碼很完美無缺啊,try–catch-finally 用得中規中矩,尤其是檔名 牛逼.txt 很亮。不用寫註釋都能明白這段程式碼是幹嘛的:在 try 塊中讀取檔案中的內容,並一行一行地列印到控制檯。如果檔案找不到或者出現 IO 讀寫錯誤,就在 catch 中捕獲並列印錯誤的堆疊資訊。最後,在 finally 中關閉緩衝字元讀取器物件 BufferedReader,有效杜絕了資源未被關閉的情況下造成的嚴重效能後果。

在 Java 7 之前,try–catch-finally 的確是確保資源會被及時關閉的最佳方法,無論程式是否會丟擲異常。

但是呢,有經驗的讀者會從上面這段程式碼中發現 2 個嚴重的問題:

1)檔名“牛逼.txt”包含了中文,需要通過 java.net.URLDecoder 類的 decode() 方法對其轉義,否則這段程式碼在執行時鐵定要丟擲檔案找不到的異常。

2)如果直接通過 new FileReader("牛逼.txt") 建立 FileReader 物件,“牛逼.txt”需要和專案的 src 在同一級目錄下,否則同樣會丟擲檔案找不到的異常。但大多數情況下,(配置)檔案會放在 resources 目錄下,便於編譯後文件出現在 classes 目錄下,見下圖。

為了解決以上 2 個問題,我們需要對程式碼進行優化:

public class TrycatchfinallyDecoder {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            String path = TrycatchfinallyDecoder.class.getResource("/牛逼.txt").getFile();
            String decodePath = URLDecoder.decode(path,"utf-8");
            br = new BufferedReader(new FileReader(decodePath));

            String str = null;
            while ((str =br.readLine()) != null) {
                System.out.println(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

執行這段程式碼,程式就可以將檔案中的內容正確輸出到控制檯。但如果你對“整潔”這個詞心生嚮往的話,會感覺這段程式碼非常臃腫,尤其是 finally 中的程式碼,就好像一個灌了 12 瓶雪花啤酒的大肚腩。

網上看到一幅 Python 程式設計師調侃 Java 程式設計師的神圖,直接 copy 過來(侵刪),逗你一樂:

況且,try–catch-finally 至始至終存在一個嚴重的隱患:try 中的 br.readLine() 有可能會丟擲 IOException,finally 中的 br.close() 也有可能會丟擲 IOException。假如兩處都不幸地丟擲了 IOException,那程式的除錯任務就變得複雜了起來,到底是哪一處出了錯誤,就需要花一番功夫,這是我們不願意看到的結果。

為了模擬上述情況,我們來自定義一個類 MyfinallyReadLineThrow,它有兩個方法,分別是 readLine()close(),方法體都是主動丟擲異常。

class MyfinallyReadLineThrow {
    public void close() throws Exception {
        throw new Exception("close");
    }

    public void readLine() throws Exception {
        throw new Exception("readLine");
    }
}

然後我們在 main() 方法中使用 try-finally 的方式呼叫 MyfinallyReadLineThrow 的 readLine()close() 方法:

public class TryfinallyCustomReadLineThrow {
    public static void main(String[] args) throws Exception {
        MyfinallyReadLineThrow myThrow = null;
        try {
            myThrow = new MyfinallyReadLineThrow();
            myThrow.readLine();
        } finally {
            myThrow.close();
        }
    }
}

執行上述程式碼後,錯誤堆疊如下所示:

Exception in thread "main" java.lang.Exception: close
    at com.cmower.dzone.trycatchfinally.MyfinallyOutThrow.close(TryfinallyCustomOutThrow.java:17)
    at com.cmower.dzone.trycatchfinally.TryfinallyCustomOutThrow.main(TryfinallyCustomOutThrow.java:10)

readLine() 方法的異常資訊竟然被 close() 方法的堆疊資訊吃了,這必然會讓我們誤以為要調查的目標是 close() 方法而不是 readLine()——儘管它也是應該懷疑的物件。

但自從有了 try-with-resources,這些問題就迎刃而解了,只要需要釋放的資源(比如 BufferedReader)實現了 AutoCloseable 介面。有了解決方案之後,我們來對之前的 finally 程式碼塊進行瘦身。

try (BufferedReader br = new BufferedReader(new FileReader(decodePath));) {
    String str = null;
    while ((str =br.readLine()) != null) {
        System.out.println(str);
    }
} catch (IOException e) {
    e.printStackTrace();
}

你瞧,finally 程式碼塊消失了,取而代之的是把要釋放的資源寫在 try 後的 () 中。如果有多個資源(BufferedReader 和 PrintWriter)需要釋放的話,可以直接在 () 中新增。

try (BufferedReader br = new BufferedReader(new FileReader(decodePath));
     PrintWriter writer = new PrintWriter(new File(writePath))) {
    String str = null;
    while ((str =br.readLine()) != null) {
        writer.print(str);
    }
} catch (IOException e) {
    e.printStackTrace();
}

如果你想釋放自定義資源的話,只要讓它實現 AutoCloseable 介面,並提供 close() 方法即可。

public class TrywithresourcesCustom {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource();) {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("關閉自定義資源");
    }
}

程式碼執行後輸出的結果如下所示:

關閉自定義資源

是不是很神奇?我們在 try () 中只是 new 了一個 MyResource 的物件,其他什麼也沒幹,但偏偏 close() 方法中的輸出語句執行了。想要知道為什麼嗎?來看看反編譯後的位元組碼吧。

class MyResource implements AutoCloseable {
    MyResource() {
    }

    public void close() throws Exception {
        System.out.println("關閉自定義資源");
    }
}

public class TrywithresourcesCustom {
    public TrywithresourcesCustom() {
    }

    public static void main(String[] args) {
        try {
            MyResource resource = new MyResource();
            resource.close();
        } catch (Exception var2) {
            var2.printStackTrace();
        }

    }
}

咦,編譯器竟然主動為 try-with-resources 進行了變身,在 try 中呼叫了 close() 方法。

接下來,我們在自定義類中再新增一個 out() 方法,

class MyResourceOut implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("關閉自定義資源");
    }

    public void out() throws Exception{
        System.out.println("沉默王二,一枚有趣的程式設計師");
    }
}

這次,我們在 try 中呼叫一下 out() 方法:

public class TrywithresourcesCustomOut {
    public static void main(String[] args) {
        try (MyResourceOut resource = new MyResourceOut();) {
            resource.out();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

再來看一下反編譯的位元組碼:

public class TrywithresourcesCustomOut {
    public TrywithresourcesCustomOut() {
    }

    public static void main(String[] args) {
        try {
            MyResourceOut resource = new MyResourceOut();

            try {
                resource.out();
            } catch (Throwable var5) {
                try {
                    resource.close();
                } catch (Throwable var4) {
                    var5.addSuppressed(var4);
                }

                throw var5;
            }

            resource.close();
        } catch (Exception var6) {
            var6.printStackTrace();
        }

    }
}

這次,catch 塊中主動呼叫了 resource.close(),並且有一段很關鍵的程式碼 var5.addSuppressed(var4)。它有什麼用處呢?當一個異常被丟擲的時候,可能有其他異常因為該異常而被抑制住,從而無法正常丟擲。這時可以通過 addSuppressed() 方法把這些被抑制的方法記錄下來。被抑制的異常會出現在丟擲的異常的堆疊資訊中,也可以通過 getSuppressed() 方法來獲取這些異常。這樣做的好處是不會丟失任何異常,方便我們開發人員進行除錯。

哇,有沒有想到我們之前的那個例子——在 try-finally 中,readLine() 方法的異常資訊竟然被 close() 方法的堆疊資訊吃了。現在有了 try-with-resources,再來看看作用和 readLine() 方法一致的 out() 方法會不會被 close() 吃掉。

close()out() 方法中直接丟擲異常:

class MyResourceOutThrow implements AutoCloseable {
    @Override
    public void close() throws Exception {
        throw  new Exception("close()");
    }

    public void out() throws Exception{
        throw new Exception("out()");
    }
}

呼叫這 2 個方法:

public class TrywithresourcesCustomOutThrow {
    public static void main(String[] args) {
        try (MyResourceOutThrow resource = new MyResourceOutThrow();) {
            resource.out();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

程式輸出的結果如下所示:

java.lang.Exception: out()
    at com.cmower.dzone.trycatchfinally.MyResourceOutThrow.out(TrywithresourcesCustomOutThrow.java:20)
    at com.cmower.dzone.trycatchfinally.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:6)
    Suppressed: java.lang.Exception: close()
        at com.cmower.dzone.trycatchfinally.MyResourceOutThrow.close(TrywithresourcesCustomOutThrow.java:16)
        at com.cmower.dzone.trycatchfinally.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:5)

瞧,這次不會了,out() 的異常堆疊資訊打印出來了,並且 close() 方法的堆疊資訊上加了一個關鍵字 Suppressed。一目瞭然,不錯不錯,我喜歡。

總結一下,在處理必須關閉的資源時,始終有限考慮使用 try-with-resources,而不是 try–catch-finally。前者產生的程式碼更加簡潔、清晰,產生的異常資訊也更靠譜。答應我好不好?別再用 try–catch-finally 了。

鳴謝

好了,我親愛的讀者朋友,以上就是本文的全部內容了,是不是感覺又學到了新知識?我是沉默王二,一枚有趣的程式設計師。原創不易,莫要白票,請你為本文點贊個吧,這將是我寫作更多優質文章的最強動力。

如果覺得文章對你有點幫助,請微信搜尋「 沉默王二 」第一時間閱讀,回覆【666】更有我為你精心準備的 500G 高清教學視訊(已分門別類)。本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。