看板 ott
作者 ott(寶貝)
標題 JAVA異常機制介紹/如何正確的進行JAVA異常處理
時間 2010年02月13日 Sat. AM 10:57:17


 
   
 
 
	
[圖]



             


            作者:Maverick
            blog:http://blog.csdn.net/zhaohuabing  轉載請註明出處
           
           1  引言

           在JAVA語言出現以前,傳統的異常處理方式多採用返回值來標識程序出現的
           異常情況,這種方式雖然為程序員所熟悉,但卻有多個壞處。首先,一個AP
           I可以返回任意的返回值,而這些返回值本身並不能解釋該返回值是否代表
           一個異常情況發生了和該異常的具體情況,需要調用API的程序自己判斷並
           解釋返回值的含義。其次,並沒有一種機制來保證異常情況一定會得到處理
           ,調用程序可以簡單的忽略該返回值,需要調用API的程序員記住去檢測返
           回值並處理異常情況。這種方式還讓程序代碼變得晦澀冗長,當進行IO操作
           等容易出現異常情況的處理時,你會發現代碼的很大部分用於處理異常情況
           的switch分支,程序代碼的可讀性變得很差。

           上面提到的問題,JAVA的異常處理機制提供了很好的解決方案。通過拋出JD
           K預定義或者自定義的異常,能夠表明程序中出現了什麼樣的異常情況;而且
           JAVA的語言機制保證了異常一定會得到恰當的處理;合理的使用異常處理機
           制,會讓程序代碼清晰易懂。

           2 JAVA異常的處理機制
             
           當程序中拋出一個異常後,程序從程序中導致異常的代碼處跳出,java虛擬
           機檢測尋找和try關鍵字匹配的處理該異常的catch塊,如果找到,將控制權
           交到catch塊中的代碼,然後繼續往下執行程序,try塊中發生異常的代碼不
           會被重新執行。如果沒有找到處理該異常的catch塊,在所有的finally塊代
           碼被執行和當前線程的所屬的ThreadGroup的uncaughtException方法被調用
           後,遇到異常的當前線程被中止。

           3 JAVA異常的類層次

           JAVA異常的類層次如下圖所示:
           
           圖1 JAVA異常的類層次

           Throwable是所有異常的基類,程序中一般不會直接拋出Throwable對象,Ex
           ception和Error是Throwable的子類,Exception下面又有RuntimeException
           和一般的Exception兩類。可以把JAVA異常分為三類:
                 
           第一類是Error,Error表示程序在運行期間出現了十分嚴重、不可恢復的錯
           誤,在這種情況下應用程序只能中止運行,例如JAVA
           虛擬機出現錯誤。Error是一種unchecked
           Exception,編譯器不會檢查Error是否被處理,在程序中不用捕獲Error類型
           的異常;一般情況下,在程序中也不應該拋出Error類型的異常。

                   第二類是RuntimeException, RuntimeException 是一種
           unchecked Exception,即表示編譯器不會檢查程序是否對RuntimeException
           作了處理,在程序中不必捕獲RuntimException類型的異常,也不必在方法體
           聲明拋出RuntimeException類。RuntimeException發生的時候,表示程序中
           出現了編程錯誤,所以應該找出錯誤修改程序,
           而不是去捕獲RuntimeException。
 
                   第三類是一般的checked Exception,這也是在編程中使用最多的
           Exception,所有繼承自Exception並且不是RuntimeException的異常都是
           checkedException,如圖1中的IOException和ClassNotFoundException。
           
           JAVA語言規定必須對checked
           Exception作處理,編譯器會對此作檢查,要麼在方法體中聲明拋出checked
           Exception,要麼使用catch語句捕獲checked
           Exception進行處理,不然不能通過編譯。checked
           Exception用於以下的語義環境:
           
           (1)
           該異常發生後是可以被恢復的,如一個Internet連接發生異常被中止後,可
           以重新連接再進行後續操作。

           (2) 程序依賴於不可靠的外部條件,該依賴條件可能出錯,如系統IO。

           (3)
           該異常發生後並不會導致程序處理錯誤,進行一些處理後可以繼續後續操作
           。
           
           4 JAVA異常處理中的注意事項

           合理使用JAVA異常機制可以使程序健壯而清晰,但不幸的是,JAVA異常處理
           機制常常被錯誤的使用,下面就是一些關於Exception的注意事項:
           
           1. 不要忽略checked Exception
           請看下面的代碼:
           try
           {
             method1();  //method1拋出ExceptionA
           }
           catch(ExceptionA e)
           {
               e.printStackTrace();
           }
           上面的代碼似乎沒有什麼問題,捕獲異常後將異常打印,然後繼續執行。事
           實上在catch塊中對發生的異常情況並沒有作任何處理(打印異常不能是算是
           處理異常,因為在程序交付運行後調試信息就沒有什麼用處了)。這樣程序
           雖然能夠繼續執行,但是由於這裡的操作已經發生異常,將會導致以後的操
           作並不能按照預期的情況發展下去,可能導致兩個結果:
           一是由於這裡的異常導致在程序中別的地方拋出一個異常,這種情況會使程
           序員在調試時感到迷惑,因為新的異常拋出的地方並不是程序真正發生問題
           的地方,也不是發生問題的真正原因;
           另外一個是程序繼續運行,並得出一個錯誤的輸出結果,這種問題更加難以
           捕捉,因為很可能把它當成一個正確的輸出。
           那麼應該如何處理呢,這裡有四個選擇:
           
           (1) 處理異常,進行修復以讓程序繼續執行。

           (2)
           重新拋出異常,在對異常進行分析後發現這裡不能處理它,那麼重新拋出異
           常,讓調用者處理。

           (3)
           將異常轉換為用戶可以理解的自定義異常再拋出,這時應該注意不要丟失原
           始異常信息(見5)。

           (4) 不要捕獲異常。
           
           因此,當捕獲一個unchecked
           Exception的時候,必須對異常進行處理;如果認為不必要在這裡作處理,
           就不要捕獲該異常,在方法體中聲明方法拋出異常,由上層調用者來處理該
           異常。
           
           2. 不要一次捕獲所有的異常

           請看下面的代碼:
           try
           {
             method1();  //method1拋出ExceptionA
               method2();  //method1拋出ExceptionB
               method3();  //method1拋出ExceptionC
           }
           catch(Exception e)
           {
               ……
           }
           這是一個很誘人的方案,代碼中使用一個catch子句捕獲了所有異常,看上
           去完美而且簡潔,事實上很多代碼也是這樣寫的。但這裡有兩個潛在的缺陷
           ,一是針對try塊中拋出的每種Exception,很可能需要不同的處理和恢復措
           施,而由於這裡只有一個catch塊,分別處理就不能實現。二是try塊中還可
           能拋出RuntimeException,代碼中捕獲了所有可能拋出的RuntimeException
           而沒有作任何處理,掩蓋了編程的錯誤,會導致程序難以調試。

           下面是改正後的正確代碼:

           try
           {
             method1();  //method1拋出ExceptionA
               method2();  //method1拋出ExceptionB
               method3();  //method1拋出ExceptionC
           }
           catch(ExceptionA e)
           {
               ……
           }
           catch(ExceptionB e)
           {
               ……
           }
           catch(ExceptionC e)
           {
               ……
           }
           
           
           3. 使用finally塊釋放資源
             
           finally關鍵字保證無論程序使用任何方式離開try塊,finally中的語句都
           會被執行。在以下三種情況下會進入finally塊:

           (1) try塊中的代碼正常執行完畢。

           (2) 在try塊中拋出異常。

           (3) 在try塊中執行return、break、continue。

           因此,當你需要一個地方來執行在任何情況下都必須執行的代碼時,就可以
           將這些代碼放入finally塊中。當你的程序中使用了外界資源,如數據庫連接
           ,文件等,必須將釋放這些資源的代碼寫入finally塊中。
           
           必須注意的是,在finally塊中不能拋出異常。JAVA異常處理機制保證無論
           在任何情況下必須先執行finally塊然後在離開try塊,因此在try塊中發生
           異常的時候,JAVA虛擬機先轉到finally塊執行finally塊中的代碼,finall
           y塊執行完畢後,再向外拋出異常。如果在finally塊中拋出異常,try塊捕
           捉的異常就不能拋出,外部捕捉到的異常就是finally塊中的異常信息,而t
           ry塊中發生的真正的異常堆棧信息則丟失了。
           請看下面的代碼:
           
           Connection  con = null;
           try
           {
               con = dataSource.getConnection();
               ……
           }
           catch(SQLException e)
           {
               ……
               throw e;//進行一些處理後再將數據庫異常拋出給調用者處理
           }
           finally
           {
               try
               {
                   con.close();
               }
               catch(SQLException e)
           {
               e.printStackTrace();
               ……
           }
           }
           運行程序後,調用者得到的信息如下
           java.lang.NullPointerException
            at myPackage.MyClass.method1(methodl.java:266)
           而不是我們期望得到的數據庫異常。這是因為這裡的con是null的關係,在f
           inally語句中拋出了NullPointerException,在finally塊中增加對con是否
           為null的判斷可以避免產生這種情況。
           
           4. 異常不能影響對象的狀態

           異常產生後不能影響對象的狀態,這是異常處理中的一條重要規則。
           在一個函數
           中發生異常後,對象的狀態應該和調用這個函數之前保持一致,以確保對象
           處於正確的狀態中。
           如果對象是不可變對象(不可變對象指調用構造函數創建後就不能改變的對
           象,即
             
           創建後沒有任何方法可以改變對象的狀態),那麼異常發生後對象狀態肯定
           不會改變。如果是可變對象,必須在編程中注意保證異常不會影響對象狀態
           。

           有三個方法可以達到這個目的:

           (1)
           將可能產生異常的代碼和改變對象狀態的代碼分開,先執行可能產生異常的
           代碼,如果產生異常,就不執行改變對象狀態的代碼。

           (2)
           對不容易分離產生異常代碼和改變對象狀態代碼的方法,定義一個recover
           方法,在異常產生後調用recover方法修復被改變的類變量,恢複方法調用
           前的類狀態。

           (3)
           在方法中使用對象的拷貝,這樣當異常發生後,被影響的只是拷貝,對象本
           身不會受到影響。
           
           5. 丟失的異常

           請看下面的代碼:
           public void method2()
           {
           try
           {
               ……
               method1();  //method1進行了數據庫操作
           }
           catch(SQLException e)
           {
               ……
               throw new MyException(「發生了數據庫異常:」+e.getMessage);
           }
           }
           public void method3()
           {
               try
           {
               method2();
           }
           catch(MyException e)
           {
               e.printStackTrace();
               ……
           }
           }

           上面method2的代碼中,try塊捕獲method1拋出的數據庫異常SQLException
           後,拋出了新的自定義異常MyException。

           這段代碼是否並沒有什麼問題,
           但看一下控制台的輸出:

           MyException:發生了數據庫異常:對象名稱 'MyTable' 無效。
           at MyClass.method2(MyClass.java:232)
           at MyClass.method3(MyClass.java:255)
           原始異常SQLException的信息丟失了,這裡只能看到method2里面定義的MyE
           xception的堆棧情況;而method1中發生的數據庫異常的堆棧則看不到,如
           何排錯呢,只有在method1的代碼行中一行行去尋找數據庫操作語句了,祈
           禱method1的方法體短一些吧。

           JDK的開發者們也意識到了這個情況,在JDK1.4.1中,Throwable類增加了兩
           個構造方法,public Throwable(Throwable cause)和public
           Throwable(String message,Throwable
           cause),在構造函數中傳入的原始異常堆棧信息將會在printStackTrace方
           法中打印出來。但對於還在使用JDK1.3的程序員,就只能自己實現打印原始
           異常堆棧信息的功能了。實現過程也很簡單,只需要在自定義的異常類中增
           加一個原始異常字段,在構造函數中傳入原始異常,然後重載printStackTr
           ace方法,首先調用類中保存的原始異常的printStackTrace方法,然後再調
           用super.printStackTrace方法就可以打印出原始異常信息了。

           可以這樣定義前面代碼中出現的MyException類:
           public class MyExceptionextends Exception
           {
               //構造函數
               public SMException(Throwable cause)
               {
                   this.cause_ = cause;
               }
           
               public MyException(String s,Throwable cause)
               {
                   super(s);
                   this.cause_ = cause;
               }
               //重載printStackTrace方法,打印出原始異常堆棧信息
               public void printStackTrace()
               {
                   if (cause_ != null)
                   {
                       cause_.printStackTrace();
                   }
                   super.printStackTrace(s);
               }
           
               public void printStackTrace(PrintStream s)
               {
                   if (cause_ != null)
                   {
                       cause_.printStackTrace(s);
                   }
                   super.printStackTrace(s);
               }
           
               public void printStackTrace(PrintWriter s)
               {
                   if (cause_ != null)
                   {
                       cause_.printStackTrace(s);
                   }
                   super.printStackTrace(s);
               }
                //原始異常
                private Throwable cause_;
           }
           
           6. 不要使用同時使用異常機制和返回值來進行異常處理

           下面是我們項目中的一段代碼
           try
           {
               doSomething();
           }
           catch(MyException e)
           {
           if(e.getErrcode == -1)
           {
               ……
           }
           if(e.getErrcode == -2)
           {
              ……
           }
           ……
           }
           假如在過一段時間後來看這段代碼,你能弄明白是什麼意思嗎?混合使用JA
           VA異常處理機制和返回值使程序的異常處理部分變得「醜陋不堪」,並難以
           理解。如果有多種不同的異常情況,就定義多種不同的異常,而不要像上面
           代碼那樣綜合使用Exception和返回值。
           修改後的正確代碼如下:
           try
           {
               doSomething();  //拋出MyExceptionA和MyExceptionB
           }
           catch(MyExceptionA e)
           {
           ……
           }
           catch(MyExceptionB e)
           {
               ……
           }
           
           
           7. 不要讓try塊過於龐大

           出於省事的目的,很多人習慣於用一個龐大的try塊包含所有可能產生異常
           的代碼,

           這樣有兩個壞處:

           閱讀代碼的時候,在try塊冗長的代碼中,不容易知道到底是哪些代碼會拋
           出哪些異常,不利於代碼維護。
           使用try捕獲異常是以程序執行效率為代價的,將不需要捕獲異常的代碼包

           含在try塊中,影響了代碼執行的效率。
     
      參考資料
      [1]   Joshua Bloch  Effective Java Programming Language Guide
      [2]   http://java.sun.com/
     
      發表於 @ 2006年05月08日 11:37:00 | 評論( 0 ) | 舉報| 收藏



	
http://blog.csdn.net/.../archive/2006/05/08/712617.aspx







 


 
※ 編輯: ott 時間: 2013-12-15 02:19:15
※ 看板: ott 文章推薦值: 0 目前人氣: 0 累積人氣: 513 
分享網址: 複製 已複製
guest
x)推文 e)編輯 d)刪除 ^x)轉錄 同主題: =)首篇 [)上篇 ])下篇