Java 進階之路:異常處理的內在原理及優雅的處理方式

永遠不要期待程式在完全理想的狀態下執行,異常往往不期而遇,如果沒有完善的異常處理機制,後果可能是災難性的。對於 Java 工程師而言,合理地處理異常是一種基本而重要的能力,然而,在近來的面試中,筆者發現很多應聘者對異常處理的內在原理幾無瞭解,現場手寫的異常處理程式碼也極為“原始”。

鑑於此,筆者將透過本場 Chat 為讀者呈現 Java 異常處理的內在原理、處理原則及優雅的處理方式。主要內容如下:

Java 異常的層次結構和處理機制

Java 異常表與異常處理的內在原理

。Java 異常處理的基本原則

優雅地處理 Java 異常案例

1。 Java 異常簡介

對於 Java 工程師而言,異常應該並不陌生,作為本場 Chat 的引入部分,對 Java 異常的基礎知識僅作簡要回顧,本文主體將聚焦於深入解讀 Java 異常的底層原理和異常處理實踐。

1。1 Java 異常類層次結構

在 Java 中,所有的異常都是由 Throwable 繼承而來,換言之,Throwable 是所有異常類共同的“祖先”,層次結構圖如下所示(注:Error、Exception 的子類及其孫子類只列出了部分):

Java 進階之路:異常處理的內在原理及優雅的處理方式

1。2 Java 異常類相關的基本概念

Throwable

作為所有異常類共同的“祖先”,Throwable 在“下一代”即分化為兩個分支:Exception(異常)和 Error(錯誤),二者是 Java 異常處理的重要子類。

Error

Error 類層次結構用於描述 Java 執行時系統的內部錯誤和資源耗盡錯誤,這類錯誤是程式無法處理的嚴重問題,一旦出現,除了通告給使用者並儘可能安全終止程式外,別無他法。

常見的錯誤如:

JVM 記憶體資源耗盡時出現的 OutOfMemoryError

棧溢位時出現的 StackOverFlowError

類定義錯誤 NoClassDefFoundError

這些錯誤表示故障發生於虛擬機器自身、或者發生在虛擬機器試圖執行應用時,它們在應用程式的控制和處理能力之外,一旦發生,Java 虛擬機器一般會選擇執行緒終止。

Exception

相較於 Error,Exception 類層次結構所描述的異常更需要 Java 程式設計者關注,因為它是程式本身可以處理的。Exception 類的“下一代”分化為兩個分支:

RuntimeException + 其它異常

劃分兩個分支的原則為:

由程式錯誤導致的異常屬於 RuntimeException;

而程式本身沒有問題,但由於 I/O 錯誤之類問題導致的異常屬於其它異常。

關於異常和錯誤的區別:通俗地講,異常是程式本身可以處理的,而錯誤則是無法處理的。

可檢查異常

可檢查異常也稱為已檢查異常(Checked Exception),這類異常是編譯器要求必須處置的異常。在工程實踐中,程式難免出現異常,其中一些異常是可以預計和容忍的,比如:

讀取檔案的時候可能出現檔案不存在的情況(FileNotFoundException),但是,並不希望因此就導致程式結束,那怎麼辦呢?

通常採用捕獲異常(try-catch)或者丟擲異常(throws 丟擲,由呼叫方處理)的方式來處理。

可檢查異常雖然也是異常,但它具備一些重要特徵:可預計、可容忍、可檢查、可處理。因此,一旦發生這類異常,就必須採取某種方式進行處理。

Java 語言規範將派生於 Error 類或 RuntimeException 類之外的所有異常都歸類為可檢查異常,Java 編譯器會檢查它,如果不做處理,無法透過編譯。

不可檢查異常

與可檢查異常相反,不可檢查異常(Unchecked Exception)是 Java 編譯器不強制要求處置的異常。Java 語言規範將 Error 類和 RuntimeException 及其子類歸類為不可檢查異常。

為什麼編譯器不強制要求處置呢?不是因為這類異常簡單,危害性小,而是因為這類異常是應該盡力避免出現的,而不是出現後再去補救。以 RuntimeException 類及其子類為例:

NullPointerException(空指標異常)

IndexOutOfBoundsException(下標越界異常)

IllegalArgumentException(非法引數異常)

這些異常通常是由不合理的程式設計和不規範的編碼引起的,工程師在設計、編寫程式時應儘可能避免這類異常的發生,這是可以做到的。在 IT 圈內有個不成文的原則:

如果出現 RuntimeException 及其子類異常,那麼可認為是程式設計師的錯誤。

1。3 異常處理機制

在 Java 應用程式中,異常處理機制有:丟擲異常、捕捉異常。

丟擲異常

這裡的“丟擲異常”是指主動丟擲異常。在設計、編寫程式時,我們可以預料到一些可能出現的異常,如 FileNotFoundException,有時候我們並不希望在當前方法中對其進行捕獲處理,怎麼辦呢?丟擲去,讓呼叫方去處理,透過 throw 關鍵字即可完成,如:

throw new FileNotFoundException()

關於丟擲異常,還有一個點需要補充,那就是宣告可檢查異常。在設計程式的時候,如果一個方法明確可能發生某些可檢查異常,那麼,可以在方法的定義中帶上這些異常,如此,這個方法的

呼叫方

就必須對這些可檢查異常進行處理。

宣告異常

根據 Java 規範,如果一個 Java 方法要丟擲異常,那麼需要在這個方法後面用 throws 關鍵字明確定義可以丟擲的異常型別。倘若沒有定義,就預設該方法不丟擲任何異常。這樣的規範決定了 Java 語法必須強行對異常進行 try-catch。如下的方法簽名:

public void foo() throws FileNotFoundException { 。。。 }

暗含了兩方面的意思:

第一,該方法要丟擲 FileNotFoundException 型別的異常;

第二,除了 FileNotFoundException 外不能(根據規範)丟擲其它的異常。

那麼,如何保證沒有除 FileNotFoundException 之外的任何異常被丟擲呢?很顯然,方式有:

透過合理的設計和編碼避免出現其它異常;

如果其它異常不可完全避免(如方法內呼叫的其它方法明確可能出現異常),就需要 try-catch 其它的異常。

簡而言之,一般情況下,方法不丟擲哪些異常就要在方法內部 try-catch 這些異常。

捕獲異常

丟擲異常十分容易,丟擲去便不用再理睬,但是,在一些場景下,必須捕獲異常並進行相應的處理。如果某個異常發生後沒有在任何地方被捕獲,那麼,程式將會終止。

在 Java 中,捕獲異常涉及三個關鍵字:try、catch 和 finally。如下舉例:

try { 可能發生異常的程式碼塊} catch (某種型別的異常 e) { 對於這種異常的處理程式碼塊} finally { 處理未盡事宜的程式碼塊:如資源回收等}

2。 Java 異常表與異常處理的內在原理

在上一部分中,筆者簡要介紹了 Java 異常,這裡將從位元組碼的層面切入,剖析 Java 異常處理的內在原理。

2。1 Java 類檔案結構簡要回顧

眾所周知,Java 是一種“與平臺無關”的程式語言,其實現“平臺無關性”的基石在於虛擬機器和位元組碼的儲存格式。事實上,Java 虛擬機器並不繫結任何程式語言(包括 Java 語言),而是與“Class 檔案”這種特定的二進位制格式檔案強關聯,這種 Class 檔案包含了 Java 虛擬機器指令集、符號表等資訊。

Java 編譯器可以將 Java 程式碼編譯成儲存位元組碼的 Class 檔案,其它語言,如 JRuby 也可以透過相應的編譯器編譯為 Class 檔案。對於虛擬機器而言,並不關心 Class 檔案源自何種語言,畢竟,Class 檔案才是 Java 虛擬機器最終要執行的計算機指令的來源。

Class 檔案的格式

Class 檔案是一組以 8 位位元組為基礎單位的二進位制流,程式編譯後的資料按照嚴格的順序緊密排列,其間沒有任何分隔符。從資料結構來看,Class 檔案採用了一種類似 C 語言結構體的偽結構來儲存資料,這種偽結構只有兩種資料型別:

無符號數

。其中,表主要有方法表、欄位表和屬性表,為便於讀者理解後文的內容,在此著重介紹一下屬性表。

屬性表(attribute_info)

屬性表可以存在於 Class 檔案、欄位表、方法表中(資料結構是可以巢狀的),用於描述某些場景的專有資訊。屬性表中有個 Code 屬性,該屬性在方法表中使用,Java 程式

方法體

中的程式碼被編譯成的位元組碼指令儲存在 Code 屬性中。

異常表(exception_table)

異常表是儲存在 Code 屬性表中的一個結構,但是,這個結構並不是必須存在的,很好理解,如果方法中根本就沒有異常相關的程式碼,編譯結果中自然也不會有異常表。

2。2 異常表解讀

異常表結構

異常表的結構如下表所示。它包含 4 個欄位,含義為:

如果當位元組碼在第 start_pc 行到 end_pc 行之間(不包含第 end_pc 行)出現了型別為 catch_type 或者其子類的異常(catch_type 為指向一個 CONSTANT_Class_info 型常量的索引),則跳轉到第 handler_pc 行執行。如果 catch_type 的值為 0,則表示任意異常情況都需要轉到 handler_pc 處進行處理。

Java 進階之路:異常處理的內在原理及優雅的處理方式

注:u2 是一種資料型別,表示 2 個位元組的無符號數。

異常表是 Java 程式碼的一部分,編譯器使用異常表而不是簡單的跳轉指令來實現 Java 異常及 finally 處理機制。

處理異常的基本原理

根據前面的介紹,不難理解,具備處理異常能力的 Java 類編譯後,都會跟隨一個異常表,如果發生異常,首先在異常表中查詢對應的行(即程式碼中相應的 try{}catch(){} 程式碼塊),如果找到,則跳轉到異常處理程式碼執行,如果沒有找到,則返回(如果有 finally,須在執行 finally 之後),並複製異常給父呼叫者,接著查詢父呼叫的異常表,以此類推,直至異常被處理或者因沒有處理而導致程式終止。

2。3 異常處理例項

為了便於讀者更好的理解 Java 異常的處理,在此,結合一個簡單的例項來看一下異常表如何運作。 Java 原始碼如下(本例參考了《深入理解 Java 虛擬機器》一書):

public class Test { public int inc() { int x; try { x = 1; return x; } catch (Exception e) { x = 2; return x; } finally { x = 3; } }}

從 Java 語義來看,上述程式碼的執行路徑有以下 3 種:

如果 try 語句塊中出現了屬於 Exception 及其子類的異常,則跳轉到 catch 處理;

如果 try 語句塊中出現了不屬於 Exception 及其子類的異常,則跳轉到 finally 處理;

如果 catch 語句塊中出現了任何異常,則跳轉到 finally 處理。

由此可以分析上述程式碼可能的返回結果:

如果沒有出現異常,返回 1;

如果出現 Exception 異常,返回 2;

如果出現了 Exception 以外的異常,則非正常退出,沒有返回。

將上面的原始碼編譯為 ByteCode 位元組碼(採用的 JDK 版本為 1。8):

public int inc(); Code: 0: iconst_1 #try中x=1入棧 1: istore_1 #x=1存入第二個int變數 2: iload_1 #將第二個int變數推到棧頂 3: istore_2 #將棧頂元素存入第三個變數,即儲存try中的返回值 4: iconst_3 #finally中的x=3入棧 5: istore_1 #棧頂元素放入第二個int變數,即finally中的x=3 6: iload_2 #將第三個int變數推到棧頂,即try中的返回值 7: ireturn #當前方法返回int,即x=1 8: astore_2 #棧頂數值放入當前frame的區域性變數陣列中第三個 9: iconst_2 #catch中的x=2入棧 10: istore_1 #x=2放入第二個int變數 11: iload_1 #將第二個int變數推到棧頂 12: istore_3 #將棧頂元素存入第四個變數,即儲存catch中的返回值 13: iconst_3 #finally中的x=3入棧 14: istore_1 #finally中的x=3放入第一個int變數 15: iload_3 #將第四個int變數推到棧頂,即儲存的catch中的返回值 16: ireturn #當前方法返回int,即x=2 17: astore 4 #棧頂數值放入當前frame的區域性變數陣列中第五個 18: iconst_3 #final中的x=3入棧 19: istore_1 #final中的x=3放入第一個int變數 20: aload 4 #當前frame的區域性變數陣列中第五個放入棧頂 21: athrow #將棧頂的數值作為異常或錯誤丟擲 Exception table: from to target type 0 4 8 Class java/lang/Exception 0 4 17 any 8 13 17 any 17 19 17 any

異常表符號解釋

從上述位元組碼中可見,對於 finally 程式碼塊,編譯器為每個可能出現的分支後都放置了冗餘。並且編譯器生成了 3 個異常表記錄(在 Exception Table 中),它們分別對應 3 條可能出現的程式碼執行路徑。Exception Table 中包含了很多資訊:異常處理開始的偏移量、結束偏移量、異常捕捉的型別等。

Exception table:異常處理資訊表

from:異常處理開始的位置

to:異常處理結束的位置

target:異常

處理器

的起始位置,即 catch 開始處理的位置

type:異常型別,any 表示所有型別

位元組碼分析

首先,0~3 行,就是把整數 1 賦值給 x,並且將此時 x 的值複製一個副本到本地變量表的 Slot 中暫存,這個 Slot 裡面的值在 ireturn 指令執行前會被重新讀到棧頂,作為返回值。這時如果沒有異常,則執行 4~5 行,把 x 賦值為 3,然後返回前面儲存的 1,方法結束。如果出現異常,讀取異常表發現應該執行第 8 行,PC 暫存器指標轉向 8 行,8~16 行就是把 2 賦值給 x,然後把 x 暫存起來,再將 x 賦值為 3,然後將暫存的 2 讀到操作棧頂返回。第 17~19 行是把 x 賦值為 3,第 20~21 行是將異常放置於棧頂並丟擲,方法結束。

3。 Java 異常處理的基本原則

在異常處理的整個過程中,需要初始化新的異常物件,從呼叫棧返回,而且還需要沿著方法的呼叫鏈來傳播異常以便找到它的異常處理器,因此,相較於普通程式碼異常處理通常需要消耗更多的時間和資源。為了保證程式碼的質量,有一些原則需要遵守。

1. 細化異常的型別,避免過度泛化

儘量避免將異常統一寫成 Excetpion。原因有二:

針對 try 塊中丟擲的每種 Exception,很可能需要不同的處理和恢復措施,如果統一為 Excetpion,則只有一個 catch 塊,分別處理就不能實現。

try 塊中有可能丟擲 RuntimeException,如果程式碼中捕獲了所有可能丟擲的 RuntimeException 而沒有作任何處理,則會掩蓋程式設計錯誤,導致程式難以除錯。

try { 。。。} catch (Exception e) { // 過分泛華的異常 。。。}

2.多個異常的處理規則

子類異常的處理塊必須在父類異常處理塊的前面,否則會發生編譯錯誤。因此,在實踐中,越特殊的異常越在前面處理,越普遍的異常越在後面處理。換句話說,能處理就儘早處理,不能處理的就丟擲去。當然,對於一個應用系統來說,丟擲大量異常是有問題的,應該從程式開發角度儘可能地控制異常發生的可能。

3. 避免過大的 try 塊

避免將不會出現異常的程式碼放到 try 塊裡面。舉個例子:迴圈的場景,注意 try 程式碼塊的範圍。

// 不恰當的方式try { while(rs。hasNext()) { foo(rs。next()); }} catch (SomeException se) { 。。。}// 較為恰當的方式while(rs。hasNext()) { try { foo(rs。next()); } catch (SomeException se) { 。。。 }}

4. 延遲捕獲

延遲捕獲:對異常的捕獲和處理需要根據當前程式碼的能力來決定,如果當前方法內無法對異常做有效處理,即使出現了檢查異常也應該考慮將異常丟擲給呼叫者做處理,如果呼叫者也無法處理,理論上它也應該繼續上拋,這樣異常最終會在一個適當的位置被 catch 下來,而比起異常出現的位置,異常的捕獲和處理是延遲了很多,但同時也避免了不恰當的處理。

5. 對於可檢查異常的處理

對於可檢查異常,如果不能行之有效地處理,還不如轉換為 RuntimeException 丟擲。如此,可以讓上層的程式碼有選擇的餘地。

6.異常處理框架

在實際應用場景中,對於一個應用系統來說,應該要有自己的一套異常處理框架,如此,當異常發生時,就能得到統一的處理風格,將優雅的異常資訊反饋給使用者。舉個例子,如微信、淘寶、支付寶之類的應用,後端涉及的元件非常多,各個元件可能都有自己一套異常處理機制,如果在對接使用者(C 端)的口子上不用統一的框架進行處理,那麼呈現給使用者的異常資訊將會失控。

7. 不要忽略異常

對於捕獲的異常,可以只打個日誌,但是儘量避免什麼都不做。

try { Class。forName(“com。mysql。jdbc。Driver”);} catch (ClassNotFoundException ex) {} //忽略的異常,挖坑

8. 避免異常轉化過程丟失資訊

有時候,我們需要將捕獲的異常進行轉化,但是,在此過程中應儘量避免丟失原始資訊,如下反例:

// 丟擲異常try { 。。。} catch (IOException ioe) { throw new Exception(ioe); // 泛化了異常, 外層呼叫丟失了異常型別的優勢}// 自定義異常try { 。。。} catch (SqlException sqle) { throw new MyOwnException(); // 定義了新的異常,但是丟了原始異常資訊}

9. 生產程式碼避免 printStackTrace()

// 不好的方式try { 。。。} catch (IOException e) { e。printStackTrace();}try { 。。。} catch (IOException e) { logger。error(“message here” + e);}try {} catch (IOException e) { logger。error(“message here” + e。getMessage());}// 比較好的方式try { 。。。} catch (IOException e) { logger。error(“message here”, e);}

4。 優雅地處理 Java 異常的案例

透過普通的方式處理 Java 異常並不困難,正因如此,很多工程師忽視了異常的本質和異常處理的原則,在工程實踐中長期採用極為“原始”的方式處理異常。在本節中,筆者將基於兩個常見的案例講述如何優雅地處理 Java 異常。

4。1 檔案流操作

我們假定需要從一個名為 test。txt 的檔案中讀取檔案流,常見的寫法如下:

public static void main(String[] args) { File file = new File(“test。txt”); InputStream inputStream = new FileInputStream(file); inputStream。read(); inputStream。close(); }

上面這段程式碼無法透過編譯,因為沒有做異常處理。我們來看一下常見的異常處理的寫法:

public static void main(String[] args) { try { File file = new File(“test。txt”); InputStream inputStream = new FileInputStream(file); inputStream。read(); inputStream。close(); } catch (IOException e) { e。printStackTrace(); } }

咋眼一看,好像一個 try-catch 就能解決問題了,但有經驗的讀者很容易看出問題——關閉資源的操作應該寫在 finally 塊中,改進後如下:

public static void main(String[] args) { try { File file = new File(“test。txt”); InputStream inputStream = new FileInputStream(file); inputStream。read(); } catch (IOException e) { e。printStackTrace(); }finally { inputStream。close(); } }

還是有問題,編譯無法透過,因為 inputStream 的作用域有問題,再次改進:

public static void main(String[] args) { InputStream inputStream = null; try { File file = new File(“test。txt”); inputStream = new FileInputStream(file); inputStream。read(); } catch (IOException e) { e。printStackTrace(); } finally { try { if (inputStream != null) { inputStream。close(); } } catch (IOException e) { e。printStackTrace(); } } }

為了把 inputStream 物件的作用域帶入 finally 塊中,我們將 inputStream 的宣告放在 try 之外。但這樣做,又導致了 inputStream 可能為 null 的情形。所以,一段本來只有兩行的程式碼便演變成這樣冗長的程式碼。

思考時間

透過上述演變的過程,讀者應該意識到,導致程式碼複雜化的關鍵點所在:我們需要在 finally 這個看起來和 try 平行的程式碼塊中,引用一個變數。

如何優雅地解決問題呢,設想一下如下情形:如果在 new FileInputStream(file) 這一步就出現了問題,那麼,inputStream 物件就不需要關閉,因為它根本不存在。這種場景下,程式碼可以簡化如下:

public static void main(String[] args) { File file = new File(“test。txt”); try { InputStream inputStream = new FileInputStream(file); inputStream。read(); inputStream。close(); } catch (IOException e) { e。printStackTrace(); } }

上面的程式碼中,透過一個 try-catch 塊捕獲由 new FileInputStream(file) 引起的異常。如果建立流成功,但讀取時發生錯誤,那麼我們必須關閉流以及時釋放資源,據此,程式碼如下:

public List getNames() { File file = new File(“test。txt”); try { InputStream inputStream = new FileInputStream(file); try { inputStream。read();//核心程式碼 }finally { inputStream。close(); } } catch (IOException e) { e。printStackTrace(); } }

如上所示,透過一個巢狀的 try catch 來處理異常,同時,在 finally 中自然地引用了inputStream 物件。原本冗長的異常處理程式碼是不是簡化了很多?

4。2 JDBC 連資料庫

對於絕大多數 Java 工程師而言,透過 JDBC 連資料庫的操作應該不會陌生。幾個關鍵步驟:載入驅動、獲取連線、執行操作、解析結果集、關閉資源。

為了便於讀者理解,在此,透過一段高度模板化的程式碼來引出問題。如下例子:查詢 Student 表中的所有人的名字。

一般的工程師都會寫出如下的程式碼:

public List getNames() { ResultSet resultSet = null; PreparedStatement preparedStatement = null; Connection connection = null; try { Class。forName(“com。jdbc。mysql。Driver”); connection = DriverManager。getConnection(“jdbc:mysql://localhost/test”); preparedStatement = connection。prepareStatement(“SELECT names from Student”); resultSet = preparedStatement。executeQuery(); List names = new LinkedList(); while (resultSet。next()) { names 。add(resultSet。getString(1)); } return names ; } catch (ClassNotFoundException e) { e。printStackTrace(); } catch (SQLException e) { e。printStackTrace(); } finally { //關閉資源 try { if (resultSet != null) { resultSet。close(); } } catch (SQLException e) { e。printStackTrace(); } try { if (preparedStatement != null) { preparedStatement。close(); } } catch (SQLException e) { e。printStackTrace(); } try { if (connection != null) { connection。close(); } } catch (SQLException e) { e。printStackTrace(); } } }

為了在 finally 塊中能夠引用到 resultSet 、preparedStatement 和 connection 三個物件,需要把它們放到了 try 塊最外面。然而,這又引發了另一個問題:在到達 finally 時,這些物件可能為 null ,因此又需要加 if 判空,如此,程式碼就變成了上面那般冗長。

思考時間

回顧一下上述例子,不難發現,其中關鍵的程式碼只有四行:它們都有各自的異常丟擲。如何最佳化呢?不妨整理一下程式碼的主流程,逐層遞進,一步一步最佳化。

1。 Class。forName(“com。jdbc。mysql。Driver”); 2。 Connection connection = DriverManager。getConnection(“jdbc:mysql://localhost/test”); 3。 PreparedStatement preparedStatement = connection。prepareStatement(“SELECT names from Student ”); 4。 ResultSet resultSet = preparedStatement。executeQuery();

場景與最佳化一

程式需要去載入驅動,不妨設想:如果載入失敗了,那肯定沒有 connection 以及後面一堆操作什麼事兒了,程式應該退出,關閉資源什麼的根本不用考慮。鑑於此,只需要加 ClassNotFoundException 宣告即可,如下程式碼:

public List getNames()throws ClassNotFoundException{ Class。forName(“com。jdbc。mysql。Driver”); Connection connection = DriverManager。getConnection(“jdbc:mysql://localhost/test”); PreparedStatement preparedStatement = connection。prepareStatement(“SELECT names from Student ”); ResultSet resultSet = preparedStatement。executeQuery(); List names = new LinkedList(); while (resultSet。next()) { names。add(resultSet。getString(1)); } return names ; }

場景與最佳化二

如果驅動載入成功,connection 獲取失敗。這種情況下,也應該退出程式,也不用關閉資源,因為沒有拿到 connection 。鑑於此,只需要加 SQLException 宣告即可,程式碼如下:

public List getNames() throws ClassNotFoundException, SQLException { Class。forName(“com。jdbc。mysql。Driver”); Connection connection = DriverManager。getConnection(“jdbc:mysql://localhost/test”); PreparedStatement preparedStatement = connection。prepareStatement(“SELECT name from Student ”); ResultSet resultSet = preparedStatement。executeQuery(); List names = new LinkedList(); while (resultSet。next()) { names。add(resultSet。getString(1)); } return names; }

場景與最佳化三

如果 connection 獲取成功,但構建 preparedStatement 物件失敗。這種情況下,需要在退出前關閉 connection,但 preparedStatement 並不需要額外處理,因為根本就沒有建立資源。按照預設情況,透過一個 try catch 便可解決,程式碼如下:

public List getNames() throws ClassNotFoundException, SQLException { Class。forName(“com。jdbc。mysql。Driver”); Connection connection = DriverManager。getConnection(“jdbc:mysql://localhost/test”); try { PreparedStatement preparedStatement = connection。prepareStatement(“SELECT name from Student ”); ResultSet resultSet = preparedStatement。executeQuery(); List names = new LinkedList(); while (resultSet。next()) { names。add(resultSet。getString(1)); } return names; } finally { connection。close(); } }

場景與最佳化四

如果 preparedStatement 建立成功,但執行失敗。這種情況下,connection 和preparedStatement 都需要關閉。 按照假設情況,我們需要再加了一層 try catch,並且在最內層 finally 中關閉已經成功建立的 preparedStatement。程式碼如下:

public List getNames() throws ClassNotFoundException, SQLException { Class。forName(“com。jdbc。mysql。Driver”); Connection connection = DriverManager。getConnection(“jdbc:mysql://localhost/test”); try { PreparedStatement preparedStatement = connection。prepareStatement(“SELECT name from Student ”); try { ResultSet resultSet = preparedStatement。executeQuery(); List names = new LinkedList(); while (resultSet。next()) { names。add(resultSet。getString(1)); } return names; } finally { preparedStatement。close(); } } finally { connection。close(); } }

場景與最佳化五

如果 resultSet 建立成功,但在遍歷中出現問題,或者整個過程沒有問題,需要關閉所有資源,退出程式。進一步處理,得到如下程式碼:

public List getNames() throws ClassNotFoundException, SQLException { Class。forName(“com。jdbc。mysql。Driver”); Connection connection = DriverManager。getConnection(“jdbc:mysql://localhost/test”); try { PreparedStatement preparedStatement = connection。prepareStatement(“SELECT name from Student ”); try { ResultSet resultSet = preparedStatement。executeQuery(); try { List names = new LinkedList(); while (resultSet。next()) { names。add(resultSet。getString(1)); } return names; } finally { resultSet。close(); } } finally { preparedStatement。close(); } } finally { connection。close(); } }

案例小結

如上所示,程式碼最終的演化結果,相較於普通的處理方式簡化了很多,與此同時,不會漏關閉任何一個資源,也不必寫一堆難看的判斷空的程式碼。