1. 程式人生 > 其它 >Java異常的深入研究與分析

Java異常的深入研究與分析

目錄

1 異常講解

1.1 異常機制概述

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

1.2 異常處理的流程

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

1.3 異常的結構

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

1.3.1 Error異常

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

1.3.2 RuntimeException異常

Exception異常包括RuntimeException異常和其他非RuntimeException的異常。
RuntimeException 是一種Unchecked Exception,即表示編譯器不會檢查程式是否對RuntimeException

作了處理,在程式中不必捕獲RuntimException型別的異常,也不必在方法體宣告丟擲RuntimeException類。RuntimeException發生的時候,表示程式中出現了程式設計錯誤,所以應該找出錯誤修改程式,而不是去捕獲RuntimeException
大致有一下幾類:

  • java.lang.ArrayIndexOutOfBoundsException:陣列索引越界異常。當對陣列的索引值為負數或大於等於陣列大小時丟擲。
  • java.lang.ArithmeticException:算術條件異常。譬如:整數除零等。
  • java.lang.NullPointerException:空指標異常。當應用試圖在要求使用物件的地方使用了null時,丟擲該異常。譬如:呼叫null物件的例項方法、訪問null物件的屬性、計算null物件的長度、使用throw語句丟擲null等等
  • java.lang.NegativeArraySizeException:陣列長度為負異常
  • java.lang.ArrayStoreException:陣列中包含不相容的值丟擲的異常
  • java.lang.SecurityException:安全性異常
  • java.lang.IllegalArgumentException:非法引數異常

1.3.3 Checked Exception異常

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

  • java.lang.ClassNotFoundException:找不到類異常。當應用試圖根據字串形式的類名構造類,而在遍歷CLASSPAH之後找不到對應名稱的class檔案時,丟擲該異常。

1.4 異常處理

1.4.1 在宣告方法時候丟擲異常

語法:throws(略)
為什麼要在宣告方法丟擲異常?

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

為什麼丟擲的異常一定是已檢查異常?

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

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

1.4.2 在方法中如何丟擲異常

語法:throw(略)
丟擲什麼異常?
對於一個異常物件,真正有用的資訊是異常的物件型別,而異常物件本身毫無意義。比如一個異常物件的型別是ClassCastException,那麼這個類名就是唯一有用的資訊。所以,在選擇丟擲什麼異常時,最關鍵的就是選擇異常的類名能夠明確說明異常情況的類。
異常物件通常有兩種建構函式:一種是無引數的建構函式;另一種是帶一個字串的建構函式,這個字串將作為這個異常物件除了型別名以外的額外說明。

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

1.4.3 throw和throws的區別

throw和throws區別:

  • throw是語句丟擲一個異常,throws是方法可能丟擲異常的宣告。(用在宣告方法時,表示該方法可能要丟擲異常)
  • throw語句用在方法體內,表示丟擲異常,由方法體內的語句處理。
  • throws語句用在方法聲明後面,表示再丟擲異常,由該方法的呼叫者來處理。
  • throws主要是宣告這個方法會丟擲這種型別的異常,使它的呼叫者知道要捕獲這個異常。
  • throw是具體向外拋異常的動作,所以它是丟擲一個異常例項。

基本區別:

  • throws是用來宣告一個方法可能丟擲的所有異常資訊
  • throw則是指丟擲的一個具體的異常型別。
  • 通常在一個方法(類)的宣告處通過throws宣告方法(類)可能丟擲的異常資訊,而在方法(類)內部通過throw宣告一個具體的異常資訊。
  • throws通常不用顯示的捕獲異常,可由系統自動將所有捕獲的異常資訊拋給上級方法;
  • throw則需要使用者自己捕獲相關的異常,而後在對其進行相關包裝,最後再將包裝後的異常資訊丟擲。

對異常處理方式不同:

  • throws對異常不處理,誰呼叫誰處理,throwsException的取值範圍要大於方法內部異常的最大範圍,而cathch的範圍又要大於throwsException的範圍;
  • throw 主動丟擲自定義異常類物件. throws丟擲的是類,throw丟擲的是物件.在方法定義中表示的是陳述語氣,第三人稱單數,throw 顯然要加s。(throws 一般用作方法定義的子句)
    在函式體中要用throw,實際上是祈使句+強調,等價於DO throw ....,do +動詞原形

throw用於引發異常,可引發預定義異常和自定義異常。

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異常了。
那麼應該在宣告方法丟擲異常還是在方法中捕獲異常?
處理原則:捕捉並處理哪些知道如何處理的異常,而傳遞哪些不知道如何處理的異常

1.4.4 使用finally塊釋放資源

finally關鍵字保證無論程式使用任何方式離開try塊,finally中的語句都會被執行。在以下三種情況下會進入finally塊:

  • try塊中的程式碼正常執行完畢。
  • 在try塊中丟擲異常。
  • 在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的開發者們也意識到了這個情況,Throwable類增加了兩個構造方法,public Throwable(Throwable cause)public Throwable(String message,Throwable cause),在建構函式中傳入的原始異常堆疊資訊將會在printStackTrace方法中打印出來。

1.4.5 try{ return }finally{}中的return

程式碼講解

class Test {
    public int aaa() {
        int x = 1;
        try {
            return ++x;
        } catch (Exception e) {
        } finally {
            ++x;
        }
        return x;
    }
    public static void main(String[] args) {
        Test t = new Test();
        int y = t.aaa();
        System.out.println(y);
    }
}

執行結果是2而不是3

  • try 語句塊裡使用 return 語句,那麼 finally 語句塊還會執行
    根據已有的知識知道:
    return 是可以當作終止語句來用的,我們經常用它來跳出當前方法,並返回一個值給呼叫方法。然後該方法就結束了,不會執行return下面的語句。
    finally :無論try語句發生了什麼,無論丟擲異常還是正常執行。finally語句都會執行。
    那麼問題來了。。。。在try語句裡使用return後,finally是否還會執行?finally一定會執行的說法是否還成立?如果成立,那麼先執行return還是先執行finally?
    官網解釋:

try語句退出時肯定會執行finally語句。這確保了即使發了一個意想不到的異常也會執行finally語句塊。但是finally的用處不僅是用來處理異常——它可以讓程式設計師不會因為returncontinue、或者break語句而忽略了清理程式碼。把清理程式碼放在finally語句塊裡是一個很好的做法,即便可能不會有異常發生也要這樣做。
注意,當try或者catch的程式碼在執行的時候,JVM退出了。那麼finally語句塊就不會執行。同樣,如果執行緒在執行try或者catch的程式碼時被中斷了或者被殺死了(killed),那麼finally語句可能也不會執行了,即使整個運用還會繼續執行。

如果try語句裡有return,那麼程式碼的行為如下:

  1. 如果有返回值,就把返回值儲存到區域性變數中
  2. 執行jsr指令跳到finally語句裡執行
  3. 執行完finally語句後,返回之前儲存在區域性變量表裡的值

從上面的官方說明,我們知道無論try裡執行了return語句、break語句、還是continue語句,finally語句塊還會繼續執行。同時,在stackoverflow裡也找到了一個答案,我們可以呼叫System.exit()來終止它:

finally will be called.
The only time finally won’t be called is: if you call System.exit(), another thread interrupts current one, or if the JVM crashes first.

另外,在java的語言規範有講到,如果在try語句裡有return語句,finally語句還是會執行。它會在把控制權轉移到該方法的呼叫者或者構造器前執行finally語句。也就是說,使用return語句把控制權轉移給其他的方法前會執行finally語句
其實,此處還牽扯到了值傳遞和引用傳遞關係問題

  • try 塊:用於捕獲異常。其後可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。
  • catch 塊:用於處理try捕獲到的異常。
  • finally 塊:無論是否捕獲或處理異常,finally塊裡的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執行。在以下4種特殊情況下,finally塊不會被執行:
    1)在finally語句塊中發生了異常。
    2)在前面的程式碼中用了System.exit()退出程式。
    3)程式所在的執行緒死亡。
    4)關閉CPU。