如何善用Java異常

xy的技術圈發表於2018-09-27

Java的異常算是Java語言的一個特色了。也是在日常編碼中會經常使用到的東西。但你真的瞭解異常嗎?

這裡有一些關於異常的經典面試題:

  • Java與異常相關的類結構和主要繼承關係是怎樣的?
  • Java7在關於異常的語法上做了什麼改進?
  • 什麼是執行時異常和宣告式異常?它們有什麼區別?
  • 什麼是“異常丟失(異常覆蓋)”問題?
  • 什麼是異常鏈?
  • 什麼是返回值覆蓋?
  • 編寫異常時的一些最佳實踐?

如果以上問題的答案你都能瞭然與胸,那麼恭喜你,已經很熟悉Java異常這一塊了。

如果一些問題還弄不清楚?沒關係,看完這篇文章就可以了。

異常的層次結構

先上圖

如何善用Java異常

拋開下面那些異常不談,我們的關注點可能主要在四個類上:

  • Throwable
  • Error
  • Exception
  • RuntimeException

其中,因為Error代表“錯誤”,多為比較嚴重的錯誤。如果你瞭解JVM,應該對OutOfMemoryErrorStackOverflowError這兩個類比較熟悉。

一般我們在寫程式碼時,可能用的比較多的是Exception類和RuntimeException類。

那到底是繼承Exception類好還是繼承RuntimeException類好呢?後面我們在“編寫異常的最佳實踐”小節會講到。

Java7與異常

Java7對異常做了兩個改進。第一個是try-with-resources,第二個是catch多個異常

try-with-resources

所謂的try-with-resources,是個語法糖。實際上就是自動呼叫資源的close()函式。和Python裡的with語句差不多。

不使用try-with-resources,我們在使用io等資源物件時,通常是這樣寫的:

String getReadLine() throws IOException {
    BufferedReader br = new BufferedReader(fileReader);
    try {
        return br.readLine();
    } finally {
        if (br != null) br.close();
    }
}
複製程式碼

使用try-with-recources的寫法:

String getReadLine() throws IOException {
    try (BufferedReader br = new BufferedReader(fileReader)) {
        return br.readLine();
    }
}
複製程式碼

顯然,編繹器自動在try-with-resources後面增加了判斷物件是否為null,如果不為null,則呼叫close()函式的的位元組碼。

只有實現了java.lang.AutoCloseable介面,或者java.io.Closable(實際上繼隨自java.lang.AutoCloseable)介面的物件,才會自動呼叫其close()函式。

有點不同的是java.io.Closable要求一實現者保證close函式可以被重複呼叫。而AutoCloseable的close()函式則不要求是冪等的。具體可以參考Javadoc。

但是,需要注意的是try-with-resources會出現異常覆蓋的問題,也就是說catch塊丟擲的異常可能會被呼叫close()方法時丟擲的異常覆蓋掉。我們會在下面的小節講到異常覆蓋。

多異常捕捉

直接上程式碼:

public static void main(String[] args) {
    try {
        int a = Integer.parseInt(args[0]);
        int b = Integer.parseInt(args[1]);
        int c = a / b;
        System.out.println("result is:" + c);
    } catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie) {
        System.out.println("發生了以上三個異常之一。");
        ie.getMessage();
        // 捕捉多異常時,異常變數預設有final修飾,
        // 所以下面程式碼有錯:
        // ie = new ArithmeticException("test");
    }
}
複製程式碼

Suppressed

如果catch塊和finally塊都丟擲了異常怎麼辦?請看下下小節分析。

執行時異常和宣告式異常

所謂執行時異常指的是RuntimeException,你不用去顯式的捕捉一個執行時異常,也不用在方法上宣告。

反之,如果你的異常只是一個Exception,它就需要顯式去捕捉。

示例程式碼:

void test() {
    hasRuntimeException();
    try {
        hasException();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

void hasException() throws Exception {
    throw new Exception("exception");
}

void hasRuntimeException() {
    throw new RuntimeException("runtime");
}
複製程式碼

雖然從異常的結構圖我們可以看到,RuntimeException繼承自Exception。但Java會“特殊對待”執行時異常。所以如果你的程式裡面需要這類異常時,可以繼承RuntimeException

而且如果不是明確要求要把異常交給上層去捕獲處理的話,我們建議是優先使用執行時異常,因為它會讓你的程式碼更加簡潔。

什麼是異常覆蓋

正如我們前面提到的,在finally塊呼叫資源的close()方法時,是有可能丟擲異常的。與此同時我們可能在catch塊丟擲了另一個異常。那麼catch塊丟擲的異常就會被finally塊的異常“吃掉”。

看看這段程式碼,呼叫test()方法會輸出什麼?

void test() {
    try {
        overrideException();
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }
}

void overrideException() throws Exception {
    try {
        throw new Exception("A");
    } catch (Exception e) {
        throw new Exception("B");
    } finally {
        throw new Exception("C");
    }
}
複製程式碼

會輸出C。可以看到,在catch塊的B被吃掉了。

JDK提供了Suppressed的兩個方法來解決這個問題:

// 呼叫test會輸出:
// C
// A
void test() {
    try {
        overrideException();
    } catch (Exception e) {
        System.out.println(e.getMessage());
        Arrays.stream(e.getSuppressed())
                .map(Throwable::getMessage)
                .forEach(System.out::println);
    }
}

void overrideException() throws Exception {
    Exception catchException = null;
    try {
        throw new Exception("A");
    } catch (Exception e) {
        catchException = e;
    } finally {
        Exception exception = new Exception("C");
        exception.addSuppressed(catchException);
        throw exception;
    }
}
複製程式碼

異常鏈

你可以在丟擲一個新異常的時候,使用initCause方法,指出這個異常是由哪個異常導致的,最終形成一條異常鏈。

詳情請查閱公眾號之前的關於異常鏈的文章。

返回值覆蓋

跟之前的“異常覆蓋”問題類似,finally塊會覆蓋掉trycatch塊的返回值。

所以最佳實踐是不要在finaly塊使用return!!!

最佳實踐?

  1. 如果可以,儘量使用RuntimeException
  2. 儘量不要在finally塊丟擲異常或者返回值
  3. 儘量使用Java7對異常的新語法
  4. try-catch塊可以單獨抽到一個方法裡面去,讓你的程式碼更簡潔 —— 參考《程式碼整潔之道》第7章
  5. 記錄異常日誌可以結合log和printStackTrace

參考文章