如何善用Java異常
Java的異常算是Java語言的一個特色了。也是在日常編碼中會經常使用到的東西。但你真的瞭解異常嗎?
這裡有一些關於異常的經典面試題:
- Java與異常相關的類結構和主要繼承關係是怎樣的?
- Java7在關於異常的語法上做了什麼改進?
- 什麼是執行時異常和宣告式異常?它們有什麼區別?
- 什麼是“異常丟失(異常覆蓋)”問題?
- 什麼是異常鏈?
- 什麼是返回值覆蓋?
- 編寫異常時的一些最佳實踐?
如果以上問題的答案你都能瞭然與胸,那麼恭喜你,已經很熟悉Java異常這一塊了。
如果一些問題還弄不清楚?沒關係,看完這篇文章就可以了。
異常的層次結構
先上圖

拋開下面那些異常不談,我們的關注點可能主要在四個類上:
- Throwable
- Error
- Exception
- RuntimeException
其中,因為 Error
代表“錯誤”,多為比較嚴重的錯誤。如果你瞭解 JVM ,應該對 OutOfMemoryError
和 StackOverflowError
這兩個類比較熟悉。
一般我們在寫程式碼時,可能用的比較多的是 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
塊會覆蓋掉 try
和 catch
塊的返回值。
所以最佳實踐是不要在 finaly
塊使用 return
!!!