JAVA異常機制介紹/如何正確的進行JAVA異常處理

來源:互聯網
上載者:User

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

1  引言
在JAVA語言出現以前,傳統的異常處理方式多採用傳回值來標識程式出現的異常情況,這種方式雖然為程式員所熟悉,但卻有多個壞處。首先,一個API可以返回任意的傳回值,而這些傳回值本身並不能解釋該傳回值是否代表一個異常情況發生了和該異常的具體情況,需要調用API的程式自己判斷並解釋傳回值的含義。其次,並沒有一種機制來保證異常情況一定會得到處理,調用程式可以簡單的忽略該傳回值,需要調用API的程式員記住去檢測傳回值並處理異常情況。這種方式還讓程式碼變得晦澀冗長,當進行IO操作等容易出現異常情況的處理時,你會發現代碼的很大部分用於處理異常情況的switch分支,程式碼的可讀性變得很差。
上面提到的問題,JAVA的異常處理機制提供了很好的解決方案。通過拋出JDK預定義或者自訂的異常,能夠表明程式中出現了什麼樣的異常情況;而且JAVA的語言機制保證了異常一定會得到恰當的處理;合理的使用異常處理機制,會讓程式碼清晰易懂。
2 JAVA異常的處理機制
    當程式中拋出一個異常後,程式從程式中導致異常的代碼處跳出,java虛擬機器檢測尋找和try關鍵字匹配的處理該異常的catch塊,如果找到,將控制權交到catch塊中的代碼,然後繼續往下執行程式,try塊中發生異常的代碼不會被重新執行。如果沒有找到處理該異常的catch塊,在所有的finally塊代碼被執行和當前線程的所屬的ThreadGroup的uncaughtException方法被調用後,遇到異常的當前線程被中止。
3 JAVA異常的類層次
JAVA異常的類層次如所示:

圖1 JAVA異常的類層次
Throwable是所有異常的基類,程式中一般不會直接拋出Throwable對象,Exception和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的異常都是checked Exception,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塊中的代碼,finally塊執行完畢後,再向外拋出異常。如果在finally塊中拋出異常,try塊捕捉的異常就不能拋出,外部捕捉到的異常就是finally塊中的異常資訊,而try塊中發生的真正的異常堆棧資訊則丟失了。
請看下面的代碼:

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的關係,在finally語句中拋出了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裡面定義的MyException的堆棧情況;而method1中發生的資料庫異常的堆棧則看不到,如何排錯呢,只有在method1的程式碼中一行行去尋找資料庫動作陳述式了,祈禱method1的方法體短一些吧。
JDK的開發人員們也意識到了這個情況,在JDK1.4.1中,Throwable類增加了兩個構造方法,public Throwable(Throwable cause)和public Throwable(String message,Throwable cause),在建構函式中傳入的原始異常堆棧資訊將會在printStackTrace方法中列印出來。但對於還在使用JDK1.3的程式員,就只能自己實現列印原始異常堆棧資訊的功能了。實現過程也很簡單,只需要在自訂的異常類中增加一個原始異常欄位,在建構函式中傳入原始異常,然後重載printStackTrace方法,首先調用類中儲存的原始異常的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)
{
   ……
}
……
}
假如在過一段時間後來看這段代碼,你能弄明白是什麼意思嗎?混合使用JAVA異常處理機制和傳回值使程式的異常處理部分變得“醜陋不堪”,並難以理解。如果有多種不同的異常情況,就定義多種不同的異常,而不要像上面代碼那樣綜合使用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/

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.