Java異常的深入研究與分析

金絲燕網發表於2016-01-12

前言

本文是異常內容的集大成者,力求全面,深入的異常知識研究與分析。本文由金絲燕網獨家撰寫,參考眾多網上資源,經過內容辨別取捨,文字格式校驗等步驟編輯而成,以饗讀者。對於本文的內容,建議小白需要多多思考力求掌握,對於老手只需意會溫故知新。對於本文的內容,屬於基礎知識研究範疇,切勿以為讀完此文就能將異常知識掌握到家。切記:操千曲而後曉聲,觀千劍而後識器,所以我覺得沒有大量的原始碼閱讀經驗,你很難知道什麼時候需要自定義異常,什麼時候需要丟擲異常。

異常機制概述

異常機制是指當程式出現錯誤後,程式如何處理。具體來說,異常機制提供了程式退出的安全通道。當出現錯誤後,程式執行的流程發生改變,程式的控制權轉移到異常處理器。

異常處理的流程

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

異常的結構

異常的繼承結構:Throwable為基類,Error和Exception繼承Throwable,RuntimeException和IOException等繼承Exception。Error和RuntimeException及其子類成為未檢查異常(unchecked),其它異常成為已檢查異常(checked)。

Java異常的深入研究與分析(1)

Error異常

Error表示程式在執行期間出現了十分嚴重、不可恢復的錯誤,在這種情況下應用程式只能中止執行,例如JAVA 虛擬機器出現錯誤。Error是一種unchecked Exception,編譯器不會檢查Error是否被處理,在程式中不用捕獲Error型別的異常。一般情況下,在程式中也不應該丟擲Error型別的異常。

RuntimeException異常

Exception異常包括RuntimeException異常和其他非RuntimeException的異常。RuntimeException 是一種Unchecked Exception,即表示編譯器不會檢查程式是否對RuntimeException作了處理,在程式中不必捕獲RuntimException型別的異常,也不必在方法體宣告丟擲RuntimeException類。RuntimeException發生的時候,表示程式中出現了程式設計錯誤,所以應該找出錯誤修改程式,而不是去捕獲RuntimeException。

Checked Exception異常

Checked Exception異常,這也是在程式設計中使用最多的Exception,所有繼承自Exception並且不是RuntimeException的異常都是checked Exception,上圖中的IOException和ClassNotFoundException。JAVA 語言規定必須對checked Exception作處理,編譯器會對此作檢查,要麼在方法體中宣告丟擲checked Exception,要麼使用catch語句捕獲checked Exception進行處理,不然不能通過編譯。

在宣告方法時候丟擲異常

語法:throws(略)

為什麼要在宣告方法丟擲異常?

方法是否丟擲異常與方法返回值的型別一樣重要。假設方法丟擲異常卻沒有宣告該方法將丟擲異常,那麼客戶程式設計師可以呼叫這個方法而且不用編寫處理異常的程式碼。那麼,一旦出現異常,那麼這個異常就沒有合適的異常控制器來解決。

為什麼丟擲的異常一定是已檢查異常?RuntimeException與Error可以在任何程式碼中產生,它們不需要由程式設計師顯示的丟擲,一旦出現錯誤,那麼相應的異常會被自動丟擲。遇到Error,程式設計師一般是無能為力的;遇到RuntimeException,那麼一定是程式存在邏輯錯誤,要對程式進行修改;只有已檢查異常才是程式設計師所關心的,程式應該且僅應該丟擲或處理已檢查異常。而已檢查異常是由程式設計師丟擲的,這分為兩種情況:客戶程式設計師呼叫會丟擲異常的庫函式;客戶程式設計師自己使用throw語句丟擲異常。

注意:覆蓋父類某方法的子類方法不能丟擲比父類方法更多的異常,所以,有時設計父類的方法時會宣告丟擲異常,但實際的實現方法的程式碼卻並不丟擲異常,這樣做的目的就是為了方便子類方法覆蓋父類方法時可以丟擲異常。

在方法中如何丟擲異常

語法:throw(略)丟擲什麼異常?

對於一個異常物件,真正有用的資訊是異常的物件型別,而異常物件本身毫無意義。比如一個異常物件的型別是ClassCastException,那麼這個類名就是唯一有用的資訊。所以,在選擇丟擲什麼異常時,最關鍵的就是選擇異常的類名能夠明確說明異常情況的類。

異常物件通常有兩種建構函式:一種是無引數的建構函式;另一種是帶一個字串的建構函式,這個字串將作為這個異常物件除了型別名以外的額外說明。

為什麼要建立自己的異常?

當Java內建的異常都不能明確的說明異常情況的時候,需要建立自己的異常。需要注意的是,唯一有用的就是型別名這個資訊,所以不要在異常類的設計上花費精力。

throw和throws的區別

public class TestThrow

{
    public static void main(String[] args)
    {
        try
        {
            //呼叫帶throws宣告的方法,必須顯式捕獲該異常
            //否則,必須在main方法中再次宣告丟擲
            throwChecked(-3);            
        }
        catch (Exception e)
        {
            System.out.println(e.getMessage());
        }
        //呼叫丟擲Runtime異常的方法既可以顯式捕獲該異常,
        //也可不理會該異常
        throwRuntime(3);
    }
    public static void throwChecked(int a)throws Exception
    {
        if (a > 0)
        {
            //自行丟擲Exception異常
            //該程式碼必須處於try塊裡,或處於帶throws宣告的方法中
            throw new Exception("a的值大於0,不符合要求");
        }
    }
    public static void throwRuntime(int a)
    {
        if (a > 0)
        {
            //自行丟擲RuntimeException異常,既可以顯式捕獲該異常
            //也可完全不理會該異常,把該異常交給該方法呼叫者處理
            throw new RuntimeException("a的值大於0,不符合要求");
        }
    }
}

補充:throwChecked函式的另外一種寫法如下所示:

public static void throwChecked(int a)
    {
        if (a > 0)
        {
            //自行丟擲Exception異常
            //該程式碼必須處於try塊裡,或處於帶throws宣告的方法中
            try
            {
                throw new Exception("a的值大於0,不符合要求");
            }
            catch (Exception e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

注意:此時在main函式裡面throwChecked就不用try異常了。

應該在宣告方法丟擲異常還是在方法中捕獲異常?

處理原則:捕捉並處理哪些知道如何處理的異常,而傳遞哪些不知道如何處理的異常

使用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的判斷可以避免產生這種情況。

丟失的異常

請看下面的程式碼:

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的程式碼行中一行行去尋找資料庫操作語句了。

JDK的開發者們也意識到了這個情況,在JDK1.4.1中,Throwable類增加了兩個構造方法,public Throwable(Throwable cause)和public Throwable(String message,Throwable cause),在建構函式中傳入的原始異常堆疊資訊將會在printStackTrace方法中列印出來。但對於還在使用JDK1.3的程式設計師,就只能自己實現列印原始異常堆疊資訊的功能了。實現過程也很簡單,只需要在自定義的異常類中增加一個原始異常欄位,在建構函式中傳入原始異常,然後過載printStackTrace方法,首先呼叫類中儲存的原始異常的printStackTrace方法,然後再呼叫super.printStackTrace方法就可以列印出原始異常資訊了。可以這樣定義前面程式碼中出現的MyException類:

import java.io.PrintStream;
import java.io.PrintWriter;
public class MyException extends Exception
{

    private static final long serialVersionUID = 1L;
    //原始異常
    private Throwable cause;
    //建構函式
    public MyException(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();
    }

    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);
    }
}

相關文章