1. 程式人生 > 實用技巧 >07_Java異常處理機制

07_Java異常處理機制

本章章節

> 7.1 異常的基本概念
> 7.2異常類的繼承架構
> 7.3丟擲異常
> 7.4編寫自己的異常類
>7.5多型中異常的宣告丟擲原則
>7.6異常使用規則
> 7.7 斷言
.本章摘要:
(1)、丟擲異常。
(2)、停止程式執行。
(1)、在程式中丟擲異常。
(2)、指定方法丟擲異常。

  即使在編譯時沒有錯誤資訊產生,但在程式執行時,經常會出現一些執行時的錯誤,這種錯誤對Java而言是一種異常。有了異常就要有相應的處理方式。本章將介紹異常的基本概念以及相關的處理方式。

  Java的錯誤型別主要有以下3種(異常屬於執行時錯誤):

    語法錯誤(syntax errors):沒有遵循

java語言規則。

    執行錯誤(runtime errors):發生一個不可以執行的操作。

    邏輯錯誤(logic errors):沒有按照預期的方案執行。

7.1 異常的基本概念

  異常也稱為例外,是在程式執行過程中發生的、會打斷程式正常執行的事件,下面是幾種常見的異常:

    1、 算術異常(ArithmeticException)。

    2、 沒有給物件開闢記憶體空間時會出現空指標異常(NullPointerException)。

    3、 找不到檔案異常(FileNotFoundException)。

  所以在程式設計時,必須考慮到可能發生的異常事件,並做出相應的處理。這樣才能保證程式可以正常執行。

  Java的異常處理機制也秉承著面向物件的基本思想。在Java中,所有的異常都是以類的型別存在,除了內建的異常類之外,Java 也可以自定義的異常類。此外,Java的異常處理機制也允許自定義丟擲異常。關於這些概念,將在後面介紹。

  7.1.1 為何需要異常處理?

  在沒有異常處理的語言中,就必須使用ifswitch等語句,配合所想得到的錯誤狀況來捕捉程式裡所有可能發生的錯誤。但為了捕捉這些錯誤,編寫出來的程式程式碼經常有很多的if語句,有時候這樣也未必能捕捉到所有的錯誤,而且這樣做勢必導致程式執行效率的降低。

  Java的異常處理機制恰好改進了這一點。它具有易於使用、可自行定義異常類,處理丟擲的異常同時又不會降低程式執行的速度等優點。因而在

Java程式設計時,應充分地利用Java的異常處理機制,以增程序序的穩定性及效率。

  7.1.2 簡單的異常範例

  Java本身已有相當好的機制來處理異常的發生。本節先來看看Java是如何處理異常的。TestException7_1是一個錯誤的程式,它在訪問陣列時,下標值已超過了陣列下標所容許的最大值,因此會有異常發生:

範例:TestException7_1.java

public class TestException7_1{
  public static void main(String args[]){
    int arr[]=new int[5];    // 容許5個元素
    arr[10]=7;  // 下標值超出所容許的範圍
    System.out.println("end of main() method !!");
  }
}

  在編譯的時候程式不會發生任何錯誤,但是在執行到第6行時,會產生下列的錯誤資訊:

    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10

    at TestException7_1.main(TestException7_1.java:6)

  錯誤的原因在於陣列的下標值超出了最大允許的範圍。Java發現這個錯誤之後,便由系統丟擲“ArrayIndexOutOfBoundsException”異常,用來表示錯誤的原因,並停止執行程式。如果沒有編寫相應的處理異常的程式程式碼,則Java的預設異常處理機制會先丟擲異常、然後停止程式執行。

  7.1.3 異常的處理

  TestException7_1的異常發生後,Java便把這個異常拋了出來,可是丟擲來之後沒有程式程式碼去捕捉它,所以程式到第6行便結束,因此根本不會執行到第7行。如果加上捕捉異常的程式程式碼,則可針對不同的異常做妥善的處理。這種處理的方式稱為異常處理

  異常處理是由trycatchfinally三個關鍵字所組成的程式塊,其語法如下:

                    

  異常處理順序:

    1、可能出現異常的程式碼都應放在try程式碼塊中。try程式塊若是有異常發生時,程式的執行便中斷,並丟擲“異常類所產生的物件”。然後將程式控制轉交給catch塊。

    2、丟擲的物件如果屬於catch()括號內欲捕獲的異常類,則catch會捕捉此異常,然後進到catch塊裡繼續執行。一個try塊可以和多個catch塊配合以處理多個異常。多捕獲是要按照從小到大的順序進行異常宣告。

    3、無論try程式塊是否有異常產生,或者產生的異常是否與catch()括號裡的異常相同,最後一定會執行finally塊裡的程式程式碼。finally通常用作資源的釋放工作。

    finally的程式程式碼塊執行結束後,程式再回到try-catch-finally塊之後繼續執行。

  由上述的過程可知,異常捕捉的過程中做了兩個判斷:第一個是try 程式塊是否有異常產生,第二個是產生的異常是否和catch()括號內欲捕捉的異常相同

  值得一提的是,finally塊是可以省略的。如果省略了finally塊不寫,則在catch()塊執行結束後,程式跳到try-cath塊之後繼續執行。

  根據這些基本概念與執行的步驟,可以繪製出如圖7-1所示的流程圖:

7-1 異常處理的流程圖

  “異常類”指的是由程式丟擲的物件所屬的類,例如TestException7_1中出現的“ArrayIndexOutOfBoundsException”就是屬於異常類的一種。至於有哪些異常類以及它們之間的繼承關係,稍後將會做更進一步的探討。下面的程式程式碼加入了trycatch,使得程式本身具有捕捉異常與處理異常的能力。

範例:TestException7_2.java

public class TestException7_2
{
    public static void main(String args[])
    {
        try {     // 檢查這個程式塊的程式碼
            int arr[] = new int[5];
            arr[10] = 7; // 在這裡會出現異常
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("陣列下標越界!");
        } finally {     // 這個塊的程式程式碼一定會執行
            System.out.println("這裡一定會被執行!");
        }
        System.out.println("main()方法結束!");
    }
}

輸出結果:

  陣列下標越界!

  這裡一定會被執行!

  main()方法結束!

程式說明:

  1、程式第7行宣告一個arr的整型陣列,並開闢了5個數據空間。

  2、程式第8行為陣列中的第10個元素賦值,此時已經超出了該陣列本身的範圍,所以會出現異常。發生異常之後,程式語句轉到catch語句中去處理,程式通過finally程式碼塊統一結束。

  上面程式的第5~9行的try 塊是用來檢查是否會有異常發生。若有異常發生,且丟擲的異常是屬於ArrayIndexOutOfBoundsException類,則會執行第10~13行的程式碼塊。因為第8行所丟擲的異常正是ArrayIndexOutOfBoundsException類,因此第12行會輸出“陣列下標越界!”字串。由本例可看出,通過異常的機制,即使程式執行時發生問題,只要能捕捉到異常,程式便能順利地執行到最後,且還能適時的加入對錯誤資訊的提示。

  程式TestException7_2裡的第10行,如果程式捕捉到了異常,則在catch括號內的異常類ArrayIndexOutOfBoundsException之後生成一個物件e,利用此物件可以得到異常的相關資訊,下例說明了類物件e的應用:

範例:TestException7_3.java

public class TestException7_3
{
    public static void main(String args[])
    {
        try {
            int arr[] = new int[5];
            arr[10] = 7;
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("陣列下標越界!");
            System.out.println("異常:" + e); // 顯示異常物件e的內容
        }
        System.out.println("main()方法結束!");
    }
}

輸出結果:

  陣列下標越界!

  異常:java.lang.ArrayIndexOutOfBoundsException: 10

  main()方法結束!

  例子中省略了finally塊,但程式依然可以執行。在第10行中,把catch()括號內的內容想象成是方法的引數,而e就是ArrayIndexOutOfBoundsException類的物件。物件e接收到由異常類所產生的物件之後,就進到第11行,輸出“陣列下標越界!”字串,而第12行則是輸出異常所屬的種類,也就是java.lang.ArrayIndexOutOfBoundsException。而java.lang正是ArrayIndexOutOfBoundsException類所屬的包。

  由前面的知識,我們知道,直接列印物件可以輸出內容,說明該類重寫了toString()方法。所以,前面我們直接列印e,其實就是e.toString()

    System.out.println(e); èSystem.out.println(e.toString());

  除了可以直接列印異常類物件之外,還可以利用如下函式訪問異常資訊:

    getMessage():返回該異常的詳細描述字串。

    printStackTrace():將該異常的跟蹤棧資訊輸出到標準錯誤輸出。

    printStackTrace(PrintStream s):將該異常的跟蹤棧資訊輸出到指定輸出流。

    getStackTrace():返回該異常的跟蹤棧資訊。

  各函式使用方法如下:

    System.out.println(e.getMessage()); e.printStackTrace();

    e.printStackTrace(System.out); System.out.println(e.getStackTrace());

  7.1.4 異常處理機制的回顧

  當異常發生時,通常可以用兩種方法來處理,一種是交由Java預設的異常處理機制做處理。但這種處理方式,Java通常只能輸出異常資訊,接著便終止程式的執行。如TestException7_1的異常發生後,Java預設的異常處理機制會顯示出:

    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10

    at TestException7_1.main(TestException7_1.java:6)

  接著結束TestException7_1的執行。

  另一種是處理方式是自行編寫的try-catch-finally塊來捕捉異常,如TestException7_2TestException7_3。自行編寫程式程式碼來捕捉異常最大的好處是:可以靈活操控程式的流程,且可做出最適當的處理。圖7-2繪出了異常處理機制的選擇流程。

7-2 異常處理的方法

7.2 異常類的繼承架構

異常可分為兩大類:java.lang.Exceptionjava.lang.Error。這兩個類均繼承自java.lang.Throwable類。圖7-3Throwable類的繼承關係圖。

7-3 Throwable類的繼承關係圖

  習慣上將ErrorException類統稱為異常類,但這兩者本質上還是有不同的。Error類專門用來處理嚴重影響程式執行的錯誤,可是通常程式設計者不會設計程式程式碼去捕捉這種錯誤,其原因在於即使捕捉到它,也無法給予適當的處理,如JAVA虛擬機器出錯或棧溢位錯誤或記憶體洩漏錯誤等就屬於Error

  不同於Error類,Exception類包含了一般性的異常,這些異常通常在捕捉到之後便可做妥善的處理,以確保程式繼續執行,如TestException7_2裡所捕捉到的ArrayIndexOutOfBoundsException就是屬於這種異常。

  從異常類的繼承架構圖中可以看出:Exception類擴展出多個子類,其中IOExceptionRuntimeException是較常用的兩種。RuntimeException即使不編寫異常處理的程式程式碼,依然可以編譯成功,而這種異常必須是在程式執行時才有可能發生,例如:陣列的索引值超出了範圍。與RuntimeException不同的是,IOException一定要編寫異常處理的程式程式碼才行,它通常用來處理與輸入/輸出相關的操作,如檔案的訪問、網路的連線等。

  幾種常見的RuntimeExcpetion如下:

    ArrayIndexoutofBoundsException:陣列下標越界異常。

    ArithmeticException:算術異常。如整數被0除,運算得出的結果。

    NullPointerException:空指標異常。當物件沒被例項化時,訪問物件的屬性或方法。

  當異常發生時,發生異常的語句程式碼會丟擲一個異常類的例項化物件,之後此物件與catch語句中的類的型別進行匹配,然後在相應的catch中進行處理。

7.3 丟擲異常

  前兩節介紹了try_catch_finally程式塊的編寫方法,本節將介紹如何丟擲(throw)異常,以及如何由try-catch來接收所丟擲的異常。丟擲異常有下列兩種方式:

    1、程式中丟擲異常

    2、指定方法丟擲異常

  以下兩小節將介紹如何在程式中丟擲異常以及如何指定方法丟擲異常。

  7.3.1 在程式中丟擲異常

  當程式出現錯誤時,系統會自動丟擲異常,除此之外,java也允許程式自行丟擲異常,自行丟擲異常使用throw語句完成。其語法格式如下:

    throw 異常類例項物件;

  可以發現在throw後面丟擲的是一個異常類的例項物件,下面來看一個例項:

範例:TestException7_4.java

public class TestException7_4
{
    public static void main(String args[])
    {
        int a = 4, b = 0;
        try {
            if (b == 0)
                throw new ArithmeticException("一個算術異常"); // 丟擲異常
            else
                System.out.println(a + "/" + b + "=" + a / b); // 若丟擲異常,則執行此行
        } catch (ArithmeticException e) {
            System.out.println("丟擲異常為:" + e);
        }
    }
}

輸出結果:

  丟擲異常為:java.lang.ArithmeticException: 一個算術異常

程式說明:

  1、程式TestException7_4是要計算a/b的值。因b是除數,不能為0。若b0,則系統會丟擲ArithmeticException異常,代表除到0這個數。

  2、在try塊裡,利用第8行來判斷除數b是否為0。如果b=0,則執行第9行的throw語句,丟擲ArithmeticException異常。如果b不為0,則輸出a/b的值。在此例中強制把b設為0,因此try塊的第9行會丟擲異常,並由第13行的catch()捕捉到異常。

  3、丟擲異常時,throw關鍵字所丟擲的是異常類的例項物件,因此第9行的throw語句必須使用new關鍵字來產生物件。

  

  7.3.2 指定方法丟擲異常

  如果方法內的程式程式碼可能會發生異常,且方法內又沒有使用任何的程式碼塊來捕捉這些異常時,則必須在宣告方法時一併指明所有可能發生的異常,以便讓呼叫此方法的程式得以做好準備來捕捉異常。也就是說,如果方法會丟擲異常,則可將處理此異常的try-catch-finally塊寫在呼叫此方法的程式程式碼內。

  如果要由方法丟擲異常,則方法必須以下面的語法來宣告:

    方法名稱(引數…)throws 異常類1,異常類2,…

  範例TestException7_5是指定由方法來丟擲異常的,如下所示:

範例:TestException7_5.java

public class TestException7_5
{
    public static void main(String[] args)
    {
        System.out.println("商為:" + div(10, 0));
    }

    public static int div(int a, int b) throws Exception
    {
        return a / b;
    }
}

編譯結果:

  TestExeption7_5.java:7: 未報告的異常java.lang.Exception;必須對其進行捕捉或宣告以便丟擲

  System.out.println("商為:" + div(a, b));

^

  1 error

程式說明:

  如果在方法中用throws丟擲一個Exception異常,則在呼叫它的地方就必須明確地用try-catch來捕捉,否則會編譯報錯。

解決辦法:

  在TestExeption7_5程式之中,如果在呼叫div函式的時候加上try-catch的語句對其進行捕捉可以解決。如下

public class TestException7_6
{
    public static void main(String[] args)
    {
        try {
            System.out.println("商為:" + div(10, 0));
        } catch (Exception e) {
            System.out.println(e);
        }
    }

    public static int div(int a, int b) throws Exception
    {
        return a / b;
    }
}

  也可以將throws Exception換成throws ArithmeticException

  如果在main()方法後再用throws Exception宣告的話,那麼程式也是依然可以編譯通過的。也就是說在呼叫用throws丟擲異常的方法時,可以將此異常在方法中再向上傳遞,而main()方法是整個程式的起點,所以如果在main()方法處如果再用throws丟擲異常,則此異常就將交由JVM進行處理了。

7.4 編寫自己的異常類

  在JDK中提供的大量API方法之中含有大量的異常類,但這些類在實際開發中往往並不能完全的滿足設計者對程式異常處理的需要,在這個時候就需要使用者自己去定義所需的異常類了,用一個類清楚的寫出所需要處理的異常。為了處理各種異常,Java可通過繼承的方式編寫自己的異常類。因為所有可處理的異常類均繼承自Exception類,所以自定義異常類也必須繼承這個類。

  自己編寫異常類的語法如下:

class 異常名稱 extends Exception
{
  …
}

  Exception構造方法:public Exception(String message);

  通常在自定義異常類的構造方法中呼叫父類Exception的構造方法:super("message");如有必要,還需要覆蓋下列獲得錯誤資訊的方法:toString()、getMessage()、printStackTrace()

  我們可以在自定義異常類裡編寫方法來處理相關的事件,甚至可以不編寫任何語句也可正常地工作,這是因為父類Exception已提供相當豐富的方法,通過繼承,子類均可使用它們。

  接下來以一個範例來說明如何定義自己的異常類以及如何使用它們。

範例:TestException7_6.java

class DefaultException extends Exception
{
    public DefaultException(String message)
    {
        super(message);  // 呼叫Exception類的構造方法,存入異常資訊
    }

    public String toString()
    {
        return "自定義異常類的toString方法";
    }

    public String getMessage()
    {
        return "自定義異常類的getMessage方法";
    }

    public void printStackTrace()
    {
        System.out.println("自定義異常類的printStackTrace方法");
    }
}


public class TestException7_6
{
    public static void main(String args[])
    {
        try {
            double money = 8668.6d;
            if (money < Math.pow(2, 20)) {

           // 在這裡用throw直接丟擲一個DefaultException類的例項物件
                throw new DefaultException("金額不足");
            }
        } catch (DefaultException e) {
            System.out.println(e); // 顯示異常物件e的內容
            System.out.println(e.toString());
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
        System.out.println("main()方法結束!");
    }
}

7.5 多型中異常的宣告丟擲原則

  在多型中,異常的宣告丟擲有一定的原則:

  子類覆蓋父類方法,子類丟擲的異常必須為父類可視異常。即子類丟擲的異常不能多於父類丟擲的異常。例如:

class OneException extends Exception
{
}

class TwoException extends Exception
{
}

class ThreeException extends Exception
{
}

class Father
{
    void show() throws OneException, TwoException
    {
    }
}

public class Test extends Father
{
  // 所丟擲的異常不能多於父類丟擲的異常
    void show() throws TwoException, ThreeException
    {
    }

    public static void main(String[] args)
    {
        new Test().show();
    }
}

  子類實現介面,如介面定義的方法丟擲的異常存在交集,在子類實現中,不能宣告丟擲任何異常。例如:

class OneException extends Exception
{
}

class TwoException extends Exception
{
}

interface Football
{
    void play() throws OneException;
}

interface Basketball
{
    void play() throws TwoException;
}

public class Test implements Football, Basketball
{
  // 如介面存在交集,不能宣告丟擲的異常
    public void play()
    {
    }

    public static void main(String[] args)
    {
        new Test().play();
    }
}

7.6 異常使用規則

  如果在方法內處理了異常則不需要丟擲,如果需要呼叫者處理異常則需要使用throws丟擲異常。如果多個類發生了共同異常可以考慮設計異常類。一般能用簡單邏輯判斷的不要使用異常,直接用if…else結構判斷即可。不要過度使用異常、不要使用過於龐大的try塊、不要忽略捕捉到的異常。

7.7 斷言

  斷言是Java 1.4版新增的一個特性,並在該版本中增加了一個關鍵字assert。可以把斷言功能看成是異常處理的高階形式。

  所謂斷言(Assertion)是一個Java語句,布林表示式,程式設計師認為在程式執行時該表示式的值應該為true。系統通過計算該布林表示式執行斷言,若該表示式為false系統會報告一個錯誤。通過驗證斷言是true,能夠使程式設計師確信程式。

  為了提高效能,可能要關閉斷言檢查。通常在程式開發和除錯階段開啟斷言功能,在部署時關閉。

  5.0以前的JDK預設情況下,若使用assert作為識別符號,編譯器會給出警告(不是編譯錯誤),從5.0以後將是一個錯誤。

  使用斷言:

    ·斷言失敗是致命的、不可恢復的錯誤。

    ·斷言檢查僅僅用在程式開發和測試階段。

  語法如下:

    1assert boolean條件

    或者

    2assert boolean條件 : 詳細資訊

  這兩個形式都會對“條件”進行判斷,“條件”是一個布林表示式。如果判斷結果為假(false)則丟擲AssertionError。在第二種形式中,“詳細資訊”會傳進AssertionError的建構函式中並轉成一個訊息字串。

  例如,如果要進行如下的計算時:

    double y=Math.sqrt(x);

    sqrt(x)是一個開平方運算,x必須為正才不會出錯。為了檢查傳入的引數是否為正,可以使用如下的斷言語句:

    assert x>=0;

    double y=Math.sqrt(x);

    或者

    assert x >= 0 : "x >= 0";

    double y=Math.sqrt(x);

  當x為負值時, assert語句將丟擲AssertionError異常,你就可以根據異常資訊對程式的其它部分進行檢查。

  開啟和關閉斷言功能:

  預設情況下,斷言是關閉的。可以通過-enableassertions或者-ea選項來執行程式以開啟斷言:

    java -ea Myapp

  開啟或者關閉斷言是類裝載器的功能。當斷言功能被關閉時,類裝載器會跳過那些和斷言相關的程式碼,因此不會降低程式執行速度,即它們沒有任何副作用。  

  可以使用-da選項來關閉斷言功能:

  java -da Myapp

·本章摘要:

1、程式中沒有處理異常程式碼時,Java的預設異常處理機制會做下面的操作:

1)、丟擲異常。

2)、停止程式執行。

2、異常處理是由trycatchfinally三個關鍵字所組成的程式塊。

3try程式塊中若有異常發生時,程式的執行便會中斷,丟擲“由系統類所產生的物件”,並依下列的步驟來執行:

1)、丟擲的物件如果屬於catch()括號內所要捕捉的異常類,catch會捕捉此異常,然後進到catch程式塊裡繼續執行。

2)、無論try程式塊是否捕捉到異常,也不管捕捉到的異常是否與catch()括號裡的異常相同,最後都會執行finally塊裡的程式程式碼。

3)、finally中的程式碼是異常的統一出口,無論是否發生異常都會執行此段程式碼。

4、當異常發生時,有兩種處理方式:

1)、交由Java預設的異常處理機制去處理。

2)、自行編寫try-catch-finally塊來捕捉異常。

5、異常可分為兩大類:java.lang.Exceptionjava.lang.Error類。

6RuntimeException可以不編寫異常處理的程式程式碼,依然可以編譯成功,它是在程式執行時才有可能發生的;而其它的Exception一定要編寫異常處理的程式程式碼才能使程式通過編譯。

7catch()括號內,只接收由Throwable類的子類所產生的物件,其它的類均不接收。

8、丟擲異常有下列兩種方式:

1)、在程式中丟擲異常。

2)、指定方法丟擲異常。

9、程式中丟擲異常時,要用到throw這個關鍵字。

10、如果方法會丟擲異常(使用throws),則可將處理此異常的try-catch-finally塊寫在呼叫此方法的程式程式碼中。

感謝閱讀。如果感覺此章對您有幫助,卻又不想白瞟