Java基礎教程(22)--異常

一.什麼是異常

  異常就是發生在程序的執行期間,破壞程序指令的正常流程的事件。當方法中出現錯誤時,該方法會創建一個對象並將其交給運行時系統。該對象稱爲異常對象,它包含有關錯誤的信息,包括錯誤的類型和出現錯誤時程序的狀態。創建異常對象並將其交給運行時系統的行爲稱爲拋出異常。
  在方法拋出異常後,運行時系統會嘗試在調用棧中查找可以處理它的程序。調用棧是指從最開始的方法到出現錯誤的方法以及之間的所有方法列表,下圖是一個調用棧:

  運行時系統在調用棧中查找包含可以處理異常的代碼塊的方法。這個代碼塊稱爲異常處理器。搜索從發生錯誤的方法開始,並按照調用方法的相反順序繼續查找。找到適當的處理程序後,運行時系統會將異常傳遞給處理程序。如果拋出的異常對象的類型與處理程序可以處理的類型匹配,就認爲異常處理程序是合適的。
  如果運行時系統窮舉搜索調用棧上的所有方法而沒有找到適當的異常處理程序,如下圖所示,則運行時系統(以及程序)就會終止,並在控制檯上打印出異常信息,其中包括異常的類型和堆棧的內容。

二.異常的分類

  在Java中,異常對象都是派生於Throwable類的一個實例。如果Java中內置的異常類不能滿足需求,用戶可以創建自己的異常類。
  下面是Java異常層次結構的一個示意圖:

  需要注意的是,所有的異常都是由Throwable繼承而來,但在下一層立即分解爲兩個分支:Error和Exception。
  Error類層次結構描述了Java運行時系統的內部錯誤和資源耗盡錯誤。應用程序不該拋出這種類型的對象。如果出現了這樣的內部錯誤,除了通知用戶,並盡力使程序安全地終止之外,便再也無能爲力了。這種情況很少出現,也不需要我們關心。
  在設計Java程序時,需要關注Exception層次結構。這個層次結構又分解爲兩個分支:一個分支派生於RuntimeException;另一個分支包含其他異常。劃分兩個分支的規則是:由程序錯誤導致的異常屬於RuntimeException;而程序本身沒有問題,但由於像I/O錯誤這類問題導致的異常屬於其他異常。
  常見的RuntimeException有NullPointerException、ClassCastException、NumberFormatException、IndexOutOfBoundException等,這些異常都是由於編寫代碼時考慮地不全面而導致的。“如果出現RuntimeException異常,那麼就一定是你的問題”是一條相當有道理的規則。例如,應該通過檢測數組下標是否越界來避免數組下標越界異常,應該通過在使用變量之前檢測是否爲null來杜絕空指針異常的發生。
  還有一些異常,並不是由於代碼的問題。例如,當我們刪除文件時,有可能這個文件並不存在,這時候就會拋出一個異常。與RuntimeException不同,此時我們並不需要修改原有的代碼,但是我們可以在拋出異常後執行一些其他的措施,例如提示用戶檢查輸入的文件路徑。
  Java語言規範將派生於Error類或RuntimeException類的所有異常稱爲非受檢異常,所有其他的異常稱爲受檢異常。受檢異常必須要進行處理,而非受檢異常既可以處理,也可以不處理。

三.捕獲並處理異常

  本節將介紹如何使用try、catch和finally塊來處理異常。此外,在Java SE 7中,還引入了帶資源的try語句,它適用於那些使用可關閉資源的情景。

1.try塊

  構建一個異常處理器的第一步就是使用try塊包圍可能出現異常的代碼。看下面的例子:

public void createFile() {
    File file = new File("D:\\bar.txt");
    try {
        if(file.createNewFile()) {
            System.out.println("Create file successfully!");
        }
    }
    catch and finally blocks ...
}

  上面的createFile方法的作用是在D盤根目錄下創建一個bar.txt文件。在調用File類的createNewFile方法時,可能會出現一個IOException。而這個異常是一個受檢異常,必須對它進行處理,因此我們使用一個try塊將可能出現異常的語句包圍。如果try塊中發生異常,則該異常由與其關聯的異常處理程序處理。

2.catch塊

  通過在try塊後使用catch塊來提供異常處理程序。如果try塊中的代碼可能出現多種異常,可以使用多個catch塊來分別對應不同的異常:

try {
    // ...
} catch (ExceptionType name) {
    // ...
} catch (ExceptionType name) {
    // ...
}

  如果在try語句塊中的任何代碼拋出了一個在catch塊中說明的異常類,那麼

  1. 程序將跳過try塊的其餘代碼;
  2. 程序將執行catch塊中的代碼;
  3. 程序繼續執行catch塊之後的代碼。

  如果在try塊中的代碼沒有拋出任何異常,那麼程序將跳過catch塊。
  如果try塊的代碼拋出了一個與任何catch塊聲明的異常類型都不匹配的異常,那麼這個方法就會立刻退出。
  下面我們給上面的例子加上catch塊:

public void createFile() {
    File file = new File("D:\\bar.txt");
    try {
        if(file.createNewFile()) {
            System.out.println("Create file successfully!");
        }
    } catch {
        System.out.println("Program threw an exception,please check the path of file.");
    }
}

  可以在一個try語句塊中可以捕獲多個異常類型,並對不同類型的異常做出不同的處理。按照下列方式爲每個異常類型使用一個單獨的catch子句:

  在Java SE 7中,同一個catch子句中可以捕獲多個異常類型。例如,假設對應缺少文件和未知主機異常的動作是一樣的,就可以合併catch子句:

3.finally塊

  考慮這樣一個問題:我們需要與數據庫建立連接並執行一些操作,當操作數據庫時可能會出現異常,因此我們需要使用try塊包圍可能出現問題的代碼,並在catch塊中對這些問題進行處理。無論在操作數據庫時是否出現了問題,我們都需要在最後關閉與數據庫的連接來釋放資源。但關閉數據庫連接的操作既不能放在try塊中,因爲程序可能在此之前就出現異常而進入catch塊;也不能放在catch塊中,因爲程序可能並沒有出現異常。此時可以將關閉數據庫連接的操作放在finally塊中。不管是否有異常被捕獲,finally子句中的代碼都會被執行。因此,finally塊經常被用來關閉資源。在下一小節中,我們將會看到一個更加優雅的關閉資源的方式。
  上面的例子可以表示爲:

try {
    // connect to database
    // operate database
} catch {
    // handle exception
} finally {
    // close connection
}

  try語句可以只有finally子句,而沒有catch子句。例如:

try {
    // connect to database
    // operate database
} finally {
    // close connection
}

  在上面的try-finally結構中,無論在try語句塊中是否遇到異常,finally塊中的語句都會被執行。如果try塊中拋出一個異常,異常會在finally塊中的語句執行完之後將異常重新拋出。但是,如果finally塊中也出現異常,那麼try塊中拋出的異常將會被丟棄,程序將會拋出finally塊中的異常。

finally塊中的return語句

  我們知道,finally塊中的語句一定會執行,那麼是否try塊或catch塊中的return語句會被finally塊中的return語句(如果有的話)覆蓋呢?下面來看一個例子:

public static String returnInFinally() {
        try {
            return "try";
        } finally {
            return "finally";
        }
    }

  調用這個方法,返回值是"finally"而不是"try"。也就是說,finally塊中的return語句會覆蓋try或catch塊中的return語句。當try塊或catch塊中的代碼執行至return語句時,程序會進入finally塊中繼續執行,最後執行finally塊中的return語句。
  finally塊中的return語句不僅會覆蓋try塊和catch塊內的返回值,還會丟棄try塊或catch塊中的異常,就像異常沒有發生一樣。例如:

public static int returnInFinally() {
        try {
            int i = 2 / 0;
            return i;
        } finally {
            return 1;
        }
    }

  上面的代碼中,2/0將會觸發ArithmeticException,但是由於finally塊中有return語句,因此這個異常將會被丟棄。
  一般來說,應該儘量避免在finally塊中使用return語句或拋出異常,除非確實有必要這麼做。

4.帶資源的try語句

  帶資源的try語句是指聲明瞭一個或多個資源的try語句。資源是在程序結束時必須關閉的對象。帶資源的try語句可以保證在try塊結束時關閉資源。任何實現了AutoCloseable接口的對象都可以被看作是資源。
  下面的方法從文件中讀取第一行,它使用了一個BufferedReader來從文件中讀取數據,BufferedReader是程序完成後必須關閉的資源:

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

  在此示例中,try語句中聲明的資源是一個BufferedReader對象。聲明語句出現在try關鍵字後面的括號內。BufferedReader類實現了接口AutoCloseable。因爲BufferedReader實例是在try語句中聲明的,所以無論try語句是正常完成還是出現異常(readLine方法可能會拋出IOException),它都將被關閉。可以這樣理解,編譯器會自動爲我們生成一個finally塊並調用資源的close方法來關閉資源。
  還可以指定多個資源,例如:

try(Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8");
    PrintWriter out = new PrintWriter("out.txt")) {
    while(in.hasNext()) {
        out.println(in.next().toUpperCase());
    }
}

  當從try塊中(無論是否出現異常)退出時,在try語句中所聲明的資源的close方法都會被自動調用,並且是按與資源聲明相反的順序來調用的。
  不同於try-catch-finally和try-finally,帶資源的try語句中,如果try塊和關閉資源時同時出現異常,程序將會拋出try塊中的異常,而關閉資源時出現的異常將會被抑制,可以通過異常對象的getSuppressed()方法獲取被抑制的異常(注意,這裏是“抑制”而不是“丟棄”,被丟棄的異常無法通過getSuppressed()獲取)。
  帶資源的try語句也可以有catch塊和finally塊,不過它們會在資源關閉後纔會執行。

5.一個完整的例子

  在上面的幾小節我們分別學習了try、catch和finally。現在我們來編寫一個同時包含這三部分的例子:

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("Entering try statement");
        out = new PrintWriter(new FileWriter("OutFile.txt");
    } catch (IOException e) {
        System.err.println("Caught IOException: " +  e.getMessage());             
    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

  這個方法的執行順序無非兩種:要麼try塊中的代碼出現異常,程序進入對應的catch塊並執行,最後執行finally塊中的代碼;要麼try塊中的代碼正常結束,然後執行finally塊中的代碼。

四.聲明方法拋出的異常

  前面我們討論了當程序中出現受檢異常時應該如何處理。然而,有時候我們不想處理或者需要調用當前方法的方法去處理,此時我們可以不編寫異常處理程序,但是需要將可能出現的異常聲明在方法名稱後面。聲明異常的語法如下:

modifiers returnType methodName(parameter list) throws Exception1, Exception2, ...

  例如,上面的writeList方法可以改寫爲:

public void writeList() throw IOException {
    PrintWriter out = null;
    try {
        System.out.println("Entering try statement");
        out = new PrintWriter(new FileWriter("OutFile.txt");
    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

  在上面的程序中,如果try塊中的代碼拋出異常,由於沒有對應的異常處理程序,異常將會繼續傳遞到調用writeList的方法中,在這個方法中也有兩種處理方式:要麼處理異常,要麼繼續傳遞異常。
  由於無需爲非受檢異常編寫異常處理程序,因此也就無需將非受檢異常聲明在方法名稱之後。

五.拋出異常

  有時候,我們的程序可能本身並沒有出現任何異常,但是程序已經進入了錯誤的邏輯,並且我們需要將這些信息告訴調用當前方法的程序,此時可以手動拋出一個異常。例如,我們編寫一個計算平方根的方法,這個方法接受非負整數作爲參數。如果其他程序在調用這個方法時傳遞了一個負數,那麼返回任何值都是錯誤的,此時就需要我們拋出一個異常,告訴該程序當前的錯誤信息。
  使用throw關鍵字來手動拋出一個異常。如下所示:

public double squareRoot(int x) throws Exception{
    if(x < 0) {
        throw new Exception("Wrong argument");
    }
    return Math.sqrt(x);
}

  當在其他方法中調用這個方法時,必須處理這個可能出現的異常。

六.異常鏈

  有時我們對異常的操作可能是將其捕獲後再拋出新的異常,例如下面的例子:

public class ExceptionChainDemo {
    public static void main(String[] args) {
        try {
            method1();
        } catch (Exception e) {
            System.out.println("Second stackTrace:");
            e.printStackTrace();
        }
    }

    private static void method1() throws Exception {
        try {
            method2();
        } catch (Exception e) {
            System.out.println("First stackTrace:");
            e.printStackTrace();
            throw new Exception("Exception from method1");
        }
    }

    private static void method2() throws Exception {
        throw new Exception("Exception from method2");
    }
}

  該程序的輸出如下:

  可以看到,method2拋出異常後,在method1中將其捕獲並打印調用棧;然後重新拋出另外一個異常,並在main方法中將其捕獲並打印調用棧。在第二次打印調用棧時,之前的異常信息已經丟棄,我們只能看到method1中拋出了一個異常。
  可以通過鏈式異常來保存之前的異常信息。也就是說,可以通過之前的異常來構造新的異常。下面是有關的幾個方法:

Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)

  getCause方法可以獲取被當前異常包裝的異常。而initCause方法和另外兩個構造方法可以將需要保存的異常包裝進當前異常。例如,如果我們要保存method2中拋出的異常,可以像下面這樣修改method1:

private static void method1() throws Exception {
    try {
        method2();
    } catch (Exception e) {
        System.out.println("First stackTrace:");
        e.printStackTrace();
        throw new Exception("Exception from method1", e);
    }
}

  再次運行程序,輸入如下:

  可以看到,第二次拋出的異常中包含了第一次拋出的異常的信息。

七.自定義異常類

  在程序中,可能會遇到任何標準異常類都沒有能夠充分描述清楚的問題。在這種情況下,我們就可以自己創建一個異常類。
  如果需要創建非受檢異常,可以創建一個RuntimeException類的子類。當然,實際上很少需要創建非受檢異常,更多情況下我們創建的都是受檢異常。在創建非受檢異常時,需要繼承Exception類或其他非受檢異常類。
  下面自定義了一個受檢異常,它繼承了IOException:

public class FileFormatException extends IOException {
    public FileFormatException() {}
    public FileFormatException(String message) {
        super(message);
    }
}

  現在我們就可以在代碼中使用自己定義的異常了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章