二哥,你之前那篇 我去 switch 的文章也特么太有趣了,讀完后意猶未盡啊,要不要再寫一篇???雖然用的是 Java 13 的語法,對舊版本不太友好。但誰能保證 Java 不會再來一次重大更新呢,就像 Java 8 那樣,活生生地把 Java 6 拍死在了沙灘上。Java 8 是香,但早晚要升級,我挺你,二哥,別在乎那些反對的聲音。
這是讀者 Alice 上周特意給我發(fā)來的信息,真令我動容。的確,上次的“我去”閱讀量杠杠的,幾個大號都轉(zhuǎn)載了,包括 CSDN,次條當天都 1.5 萬閱讀。但比如“還以為你有什么新特技,沒想到用的是 Java 13”這類批評的聲音也不在少數(shù)。
不過我的心一直很大。從我寫第一篇文章至今,被噴的次數(shù)就好像頭頂上茂密的發(fā)量一樣,數(shù)也數(shù)不清。所以我決定再接再厲,帶來新的一篇“我去”。

這次不用遠程 review 了,因為我們公司也復工了。這次 review 的代碼仍然是小王的,他編寫的大部分代碼都很漂亮,嚴謹?shù)耐瑫r注釋也很到位,這令我非常滿意。但當我看到他沒用 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 用得中規(guī)中矩,尤其是文件名 牛逼.txt 很亮。不用寫注釋都能明白這段代碼是干嘛的:在 try 塊中讀取文件中的內(nèi)容,并一行一行地打印到控制臺。如果文件找不到或者出現(xiàn) IO 讀寫錯誤,就在 catch 中捕獲并打印錯誤的堆棧信息。最后,在 finally 中關閉緩沖字符讀取器對象 BufferedReader,有效杜絕了資源未被關閉的情況下造成的嚴重性能后果。
在 Java 7 之前,try–catch-finally 的確是確保資源會被及時關閉的最佳方法,無論程序是否會拋出異常。
但是呢,有經(jīng)驗的讀者會從上面這段代碼中發(fā)現(xiàn) 2 個嚴重的問題:
1)文件名“牛逼.txt”包含了中文,需要通過 java.net.URLDecoder 類的 decode() 方法對其轉(zhuǎn)義,否則這段代碼在運行時鐵定要拋出文件找不到的異常。
2)如果直接通過 new FileReader("牛逼.txt") 創(chuàng)建 FileReader 對象,“牛逼.txt”需要和項目的 src 在同一級目錄下,否則同樣會拋出文件找不到的異常。但大多數(shù)情況下,(配置)文件會放在 resources 目錄下,便于編譯后文件出現(xiàn)在 classes 目錄下,見下圖。

為了解決以上 2 個問題,我們需要對代碼進行優(yōu)化:
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();
}
}
}
}
}
運行這段代碼,程序就可以將文件中的內(nèi)容正確輸出到控制臺。但如果你對“整潔”這個詞心生向往的話,會感覺這段代碼非常臃腫,尤其是 finally 中的代碼,就好像一個灌了 12 瓶雪花啤酒的大肚腩。
網(wǎng)上看到一幅 Python 程序員調(diào)侃 Java 程序員的神圖,直接 copy 過來(侵刪),逗你一樂:

況且,try–catch-finally 至始至終存在一個嚴重的隱患:try 中的 br.readLine() 有可能會拋出 IOException,finally 中的 br.close() 也有可能會拋出 IOException。假如兩處都不幸地拋出了 IOException,那程序的調(diào)試任務就變得復雜了起來,到底是哪一處出了錯誤,就需要花一番功夫,這是我們不愿意看到的結(jié)果。
為了模擬上述情況,我們來自定義一個類 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 的方式調(diào)用 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() 方法的堆棧信息吃了,這必然會讓我們誤以為要調(diào)查的目標是 close() 方法而不是 readLine()——盡管它也是應該懷疑的對象。
但自從有了 try-with-resources,這些問題就迎刃而解了,只要需要釋放的資源(比如 BufferedReader)實現(xiàn)了 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();
}
如果你想釋放自定義資源的話,只要讓它實現(xiàn) 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("關閉自定義資源");
}
}
代碼運行后輸出的結(jié)果如下所示:
關閉自定義資源
是不是很神奇?我們在 try () 中只是 new 了一個 MyResource 的對象,其他什么也沒干,但偏偏 close() 方法中的輸出語句執(zhí)行了。想要知道為什么嗎?來看看反編譯后的字節(jié)碼吧。
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 中調(diào)用了 close() 方法。
接下來,我們在自定義類中再添加一個 out() 方法,
class MyResourceOut implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("關閉自定義資源");
}
public void out() throws Exception{
System.out.println("沉默王二,一枚有趣的程序員");
}
}
這次,我們在 try 中調(diào)用一下 out() 方法:
public class TrywithresourcesCustomOut {
public static void main(String[] args) {
try (MyResourceOut resource = new MyResourceOut();) {
resource.out();
} catch (Exception e) {
e.printStackTrace();
}
}
}
再來看一下反編譯的字節(jié)碼:
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 塊中主動調(diào)用了 resource.close(),并且有一段很關鍵的代碼 var5.addSuppressed(var4)。它有什么用處呢?當一個異常被拋出的時候,可能有其他異常因為該異常而被抑制住,從而無法正常拋出。這時可以通過 addSuppressed() 方法把這些被抑制的方法記錄下來。被抑制的異常會出現(xiàn)在拋出的異常的堆棧信息中,也可以通過 getSuppressed() 方法來獲取這些異常。這樣做的好處是不會丟失任何異常,方便我們開發(fā)人員進行調(diào)試。
哇,有沒有想到我們之前的那個例子——在 try-finally 中,readLine() 方法的異常信息竟然被 close() 方法的堆棧信息吃了?,F(xiàn)在有了 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()");
}
}
調(diào)用這 2 個方法:
public class TrywithresourcesCustomOutThrow {
public static void main(String[] args) {
try (MyResourceOutThrow resource = new MyResourceOutThrow();) {
resource.out();
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序輸出的結(jié)果如下所示:
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。一目了然,不錯不錯,我喜歡。
總結(jié)一下,在處理必須關閉的資源時,始終有限考慮使用 try-with-resources,而不是 try–catch-finally。前者產(chǎn)生的代碼更加簡潔、清晰,產(chǎn)生的異常信息也更靠譜。答應我好不好?別再用 try–catch-finally 了。

覺得有點用記得給我點贊哦!??
簡單介紹一下。10 年前,當我上大學的時候,專業(yè)被調(diào)劑到了計算機網(wǎng)絡,主要學的是 Java 編程語言,但當時沒怎么好好學,每年都要掛科兩三門;因此工作后吃了不少虧。但是最近幾年,情況發(fā)生了很大改變,你應該也能看得到我這種變化。通過堅持不懈地學習,持續(xù)不斷地輸出,我的編程基本功算得上是突飛猛進。
為了幫助更多的程序員,我創(chuàng)建了“沉默王二”這個 ID,專注于分享有趣的 Java 技術編程和有益的程序人生。一開始,閱讀量寥寥無幾,關注人數(shù)更是少得可憐。但隨著影響力的逐步擴大,閱讀量和關注人都在猛烈攀升。
你在看這篇文章的時候,應該也能發(fā)現(xiàn),我在 CSDN 上的總排名已經(jīng)來到了第 71 位,這個排名還是非常給力的。有很多讀者都說,我可以沖擊第一名,我不愿意藏著掖著,我是有這個野心的。如果你也喜歡我的文章,請記得微信搜索「沉默王二」關注我的原創(chuàng)公眾號,回復“1024”更有美團技術大佬整理的 Java 面試攻略相送,還有架構師的面試視頻哦。絕對不容錯過,期待與你的不期而遇。