切莫輕視JAVA異常處理

 
JAVA異常處理是程序開發的一個重要內容,異常處理的好壞關係到系統的健壯性和穩定度。異常處理看起來只有幾個常用語句,故有些開發人員常常會對異常處理輕視和在使用上思路模糊。近期筆者在一個開發項目中就體驗到輕視異常處理的慘痛教訓,因爲對異常沒有處理好,後果是嚴重影響系統穩定性。因此,筆者認爲異常處理並不是表面看起來的那麼簡單。本文分享在此項目過程中對異常處理的一些看法。

   
. 什麼是異常
   
JAVA程序運行時,我們常常會出現一些非正常的現象,這種情況稱爲運行錯誤。根據其性質可以分爲錯誤和異常。JAVA用面向對象的方法處理異常,首先會建立類的層次。類 Throwable位於這一類層次的最頂層,只有它的後代纔可以作爲一個異常被拋棄。類Throwable有兩個直接子類:Error Exception

   
一般來說錯誤最常見的有程序進入死循環,內存泄漏等。這種情況,程序運行時本身無法解決,只能通過其他程序干預。JAVA對應的類爲Error類。Error類對象由JAVA虛擬機生成並拋棄(通常JAVA程序不對這類異常進行處理)。

   
異常是程序執行時遇到的非正常情況或意外行爲。一般以下這些情況都可以引發異常:代碼或調用的代碼(如共享庫)中有錯誤,操作系統資源不可用,公共語言運行庫遇到意外情況(如無法驗證代碼)等等。常見的有數組下標越界,算法溢出(超出數值表達範圍),除數爲零,無效參數、內存溢出等。這種情況不像錯誤類那樣,程序運行時本身可以解決,由異常代碼調整程序運行方向,使程序仍可繼續運行直至正常結束。

    JAVA
對應的類爲Exception類。Exception類對象是JAVA程序處理或拋棄的對象。它有各種不同的子類分別對應於不同類型的異常。 JAVA編譯器要求程序必須捕獲或聲明所有的非運行時異常,但對運行時異常可以不做處理。其中類RuntimeException代表運行時由JAVA擬機生成的異常,原因是編程錯誤。其它則爲非運行時異常,原因是程序碰到了意外情況,如輸入輸出異常IOException等。

   
. 異常處理程序的功效
   
當在程序運行過程中發生的異常事件,這些異常事件的發生將阻止程序的正常運行。爲了加強程序的穩定性,程序設計時,必須考慮到可能發生的異常事件並做出相應的處理。因此,異常處理程序就是能夠讓系統在出現異常的情況下恢復過來的程序。

    JAVA
通過面向對象的程序來處理異常。在一個程序的運行過程中,如果發生了異常,則這個程序生成代表該異常的一個對象,並把它交給運行時系統,運行時系統尋找相應的代碼來處理這一異常。我們把生成異常對象並把它提交給運行時系統的過程稱爲拋出異常(Throw)。異常拋出後,運行時系統從生成對象的代碼開始,沿程序的調用棧逐層回溯查找,直到找到包含相應處理的程序,並把異常對象交給該程序爲止,這個過程稱爲捕獲異常(Catch)

   
爲了使異常處理更出色地發揮它的功效,程序員需要對所有可能發生的異常,預製各式各樣的異常類和錯誤類。它們都是從拋出異常類Throwable繼承而來的,它派生出兩個類ErrorException

   
Error派生的子類命名爲XXXError,其中詞XXX是描述錯誤類型的詞。由Exception派生的子類命名爲XXXException,其中XXX是描述異常類型的詞。Error類處理的是運行使系統發生的內部錯誤,是不可恢復的,唯一的辦法是終止運行程序。因此,一般來說開發人員只要掌握和處理好Exception類就可以了。對於運行時異常RuntimeException,我們沒必要專門爲它寫一個異常控制器,因爲它們是由於編程不嚴謹而造成的邏輯錯誤。只要出現終止,它會自動得到處理。需要開發人員進行異常處理的是那些非運行期異常。

  
三、異常處理的兩種思路
    JAVA
異常處理的一個好處就是允許我們在一個地方將精力集中在要解決的問題上,然後在另一個地方對待來自那個代碼內部的錯誤。我們只需要在那個可能發生異常的地方設置“監視區”,我們對此區域日夜監視着,通常它是一個語句塊。同時我們還需要在另一個地方設置處理問題模塊,如“異常處理模塊”或者“異常控制器”。這樣可有效減少代碼量,並將那些用於描述具體操作的代碼與專門糾正異常的代碼分隔開。一般情況下,會讓用於讀取、寫入以及調試的代碼會變得更富有條理。

   
一般來說有兩種思路處理異常。第一種將含有異常出口的程序直接放到try塊中,然後由緊隨其後的catch塊捕捉。JAVAtrycatch語法來處理異常,將關聯有異常類的程序包含在try{}程序塊中,catch(){}關鍵字可以使用形參,用於和程序產生的異常對象結合。當調用某個程序時,引起異常事件發生的條件成立,便會拋出異常,原來的程序流程將會在此程序處中斷,然後try模塊後緊跟的catch中的形參和此異常對象完成了結合,繼而進入catch模塊中運行。 

   
這裏引用一個最簡單的例子來說明:
int myMethod(int dt) ...{
int data = 0;
try...{
    
int data = isLegal(dt);
}
catch(LowZeroException e)............{
     System.
out.println("發生數據錯誤!");
}
return data;
}
 
    第二種是不直接監聽捕捉被引用程序的異常,而是將這個異常關聯傳遞給引用程序,同時監聽捕捉工作也相應向上傳遞。
 
 
. 解讀五個異常處理語句的應用教訓
  筆者結合本次項目教訓談談JAVA異常處理的五個關鍵語句:trycatchthrowthrowsfinally。希望能與大家分享在本次項目開發遇到的問題和總結一些經驗教訓。
 
    4.1 Trycatch的教訓
  try語句用{}指定了一段代碼,該段代碼可能會拋棄一個或多個異常。catch語句的參數類似於程序的聲明,包括一個異常類型和一個異常對象。異常類型必須爲Throwable類的子類,它指明瞭catch語句所處理的異常類型,異常對象則由運行時系統在try所指定的代碼塊中生成並被捕獲,大括號中包含對象的處理,其中可以調用對象的程序。
  
    JAVA運行時系統從上到下分別對每個catch語句處理的異常類型進行檢測,直到找到類型相匹配的catch語句爲止。這裏類型匹配指catch所處理的異常類型與生成的異常對象的類型完全一致或者是它的父類。因此,catch語句的排列順序應該是從特殊到一般。也可以用一個catch語句處理多個異常類型,這時它的異常類型參數應該是這多個異常類型的父類,程序設計中要根據具體的情況來選擇catch語句的異常處理類型。
 
    異常被異常處理程序捕獲和處理,異常處理程序緊接在try塊後面,且用catch關鍵字標記,因此叫做“catch塊”。如果一個程序使用了異常規範,我們在調用它時必須使用try-catch結構來捕獲和處理異常規範所指示的異常,否則編譯程序會報錯而不能通過編譯。這正是JAVA的異常處理的傑出貢獻,它對可能發生的意外及早預防從而加強了代碼的健壯性。
 
     在這次項目中得到一個教訓是不要用一個catch語句捕獲所有的異常和試圖處理所有可能出現的異常。一個程序中可能會產生多種不同的異常,我們可以設置多個異常拋出點來解決這個問題。異常對象從產生點產生後,到被捕捉後終止生命的全過程中,實際上是一個傳值過程,所以我們需要根據實際需要來合理的控制檢測到異常的個數。catch語句表示我們預期會出現某種異常,而且希望能夠處理該異常。我們建議在catch語句中應該儘可能指定具體的異常類型,必要時使用多個catch,用於分別處理不同類的異常。實際上絕大多數異常都直接或間接從JAVA.ang.Exception派生。例如我們想要捕獲一個最明顯的異常是SQLException,這是JDBC操作中常見的異常。另一個可能的異常是IOException,因爲它要操作 OutputStreamWriter。顯然,在同一個catch塊中處理這兩種截然不同的異常是不合適的。如果用兩個catch塊分別捕獲 SQLExceptionIOException就要好多了。這就是說,catch語句應當儘量指定具體的異常類型,而不應該指定涵蓋範圍太廣的 Exception類。
 
    在此項目另一個教訓是初級開發人員總喜歡把大量的代碼放入單個try塊,這個壞習慣使我們在測試和分析問題過程中花費了大量的時間。把大量的代碼放入單個 try塊,然後再在catch語句中聲明Exception,而不是分離各個可能出現異常的段落並分別捕獲其異常。這種做法爲分析程序拋出異常的原因帶來了困難,因爲一大段代碼中有太多的地方可能拋出Exception。程序的條理性和可閱讀性也會變得非常差,因此我們需要儘量減小try塊的體積。
 
    異常處理中還有一種特殊情況---RuntimeException異常類,這個異常類和它的所有子類都有一個特性,就是異常對象一產生就被JAVA虛擬機直接處理掉,即在程序中出現throw 子句的地方便被虛擬機捕捉了。因此凡是拋出這種運行時異常的程序在被引用時,不需要用trycatch語句來處理異常。
 
 
異常處理語句的一般格式是:
try...{
// 可能產生異常的代碼
}
catch (異常對象 e) ...{
//異常 e的處理語句 }
catch (異常對象 e1) ...{
//異常 e的處理語句 }
catch (異常對象 e2) ...{
//異常 e的處理語句
}
 
    4.2 解讀Throwthrows區別
    在使用異常規範的程序聲明中,開發人員使用throw語句來拋出異常,throw總是出現在函數體中。程序會在throw語句後立即終止,它後面的語句執行不到,然後在包含它的所有try塊中從裏向外尋找含有與其匹配的catch子句的try塊。
throw語句的格式爲:
    throw new XXXException();
    由此可見,throw語句拋出的是XXX類型的異常的對象(隱式的句柄)。而catch控制器捕獲對象時要給出一個句柄 catch(XXXException e)
 
    如果一個Java程序遇到了它不能夠處理的情況,那麼它可以拋出一個異常:一個程序不僅告訴Java編譯器它能返回什麼值,還可以告訴編譯器它有可能產生什麼錯誤。JAVA爲了使開發人員準確地知道要編寫什麼代碼來捕獲所有潛在的異常,採用一種叫做throws的語法結構。它用來通知那些要調用程序的開發人員,他們可能從自己的程序裏拋出什麼樣的異常。這便是所謂的“異常規範”,它屬於程序聲明的一部分。
 
    throw 子句用來拋出異常,而throws子句用來指定異常。throw 的操作數是Throwable所有派生類,Throwable的直接子類是Exception(應捕獲的問題,應進行處理)Error(重大系統問題,一般不捕獲)。拋出異常拋出點有try{}塊、, try{}塊某個深層嵌套的作用域、try{}塊某個深層嵌套的程序中。簡單說throws是指定throw拋出的異常。
 
    throws總是出現在一個函數頭中,用來標明該成員函數可能拋出的各種異常。對大多數Exception子類來說,JAVA 編譯器會強迫你聲明在一個成員函數中拋出的異常的類型。如果你想明確地拋出一個RuntimeException,你必須用throws語句來聲明它的類型。
    例如
    void f() throws tooBig tooSmall divZero { 程序體}
    若使用下述代碼:   
    void f() [ // …
    它意味着不會從程序裏拋出異常。
 
    4.3. 巧妙應用finally使出口統一
    異常改變了程序正常的執行流程。這個道理雖然簡單,卻常常被人們忽視。我們在這次項目中就遇到這樣的情況,就是無論一個異常是否發生,必須執行某些特定的代碼。比如文件已經打開,關閉文件是必須的。再如在程序用到了SocketJDBC連接之類的資源,即使遇到了異常,正常來說是也要正確釋放佔用的資源。
 
    但是,在try所限定的代碼中,當拋棄一個異常時,其後的代碼不會被執行。在catch區中的代碼在異常沒有發生的情況下也不會被執行。爲了無論異常是否發生都要執行的代碼,爲此,JAVA提供了一個簡化這類操作的關鍵詞finally,也就是無論catch語句的異常類型是否與所拋棄的異常的類型一致, finally所指定的代碼都要被執行。Finally保證在try/catch/finally塊結束之前,執行清理任務的代碼有機會執行,它提供了統一的出口。
 
. 切莫輕視異常處理
    常常會有一些程序員習慣在編程時拖延或忘記異常處理程序的編寫。因爲輕視異常這一壞習慣是如此常見,它甚至已經影響到了JAVA本身的設計。代碼捕獲了異常卻不作任何處理,可以算得上JAVA編程中的殺手。從問題出現的頻繁程度和禍害程度來看,如果你看到了出現異常的情況,可以百分之九十地肯定代碼存在問題。
 
    最好的方法是在進行系統設計就把異常處理融合在系統中,若系統一旦實現,就很難添加異常處理功能。因此從項目一開始就應該着手進行異常處理,必須投入大精力把異常處理的策略融合到軟件產品中。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章