兩萬字詳解 Java 異常,面試再也不怕被問到!
Java 異常簡介
Java 異常是 Java 提供的一種識別及響應錯誤的一致性機制。
Java 異常機制可以使程序中異常處理代碼和正常業務代碼分離,保證程序代碼更加優雅,並提高程序健壯性。在有效使用異常的情況下,異常能清晰的回答 what, where, why 這 3 個問題:異常類型回答了 “什麼” 被拋出,異常堆棧跟蹤回答了 “在哪” 拋出,異常信息回答了 “爲什麼” 會拋出。
Java 異常架構
1. Throwable
Throwable 是 Java 語言中所有錯誤與異常的超類。
Throwable 包含兩個子類:Error(錯誤)和 Exception(異常),它們通常用於指示發生了異常情況。
Throwable 包含了其線程創建時線程執行堆棧的快照,它提供了 printStackTrace() 等接口用於獲取堆棧跟蹤數據等信息。
2. Error(錯誤)
定義:Error 類及其子類。程序中無法處理的錯誤,表示運行應用程序中出現了嚴重的錯誤。
特點:此類錯誤一般表示代碼運行時 JVM 出現問題。通常有 Virtual MachineError(虛擬機運行錯誤)、NoClassDefFoundError(類定義錯誤)等。比如 OutOfMemoryError:內存不足錯誤;StackOverflowError:棧溢出錯誤。此類錯誤發生時,JVM 將終止線程。
這些錯誤是不受檢異常,非代碼性錯誤。因此,當此類錯誤發生時,應用程序不應該去處理此類錯誤。按照 Java 慣例,我們是不應該實現任何新的 Error 子類的!
3. Exception(異常)
程序本身可以捕獲並且可以處理的異常。Exception 這種異常又分爲兩類:運行時異常和編譯時異常。
運行時異常
定義:RuntimeException 類及其子類,表示 JVM 在運行期間可能出現的異常。
特點:Java 編譯器不會檢查它。也就是說,當程序中可能出現這類異常時,倘若既 "沒有通過 throws 聲明拋出它",也 "沒有用 try-catch 語句捕獲它",還是會編譯通過。比如 NullPointerException 空指針異常、ArrayIndexOutBoundException 數組下標越界異常、ClassCastException 類型轉換異常、ArithmeticExecption 算術異常。
此類異常屬於不受檢異常,一般是由程序邏輯錯誤引起的,在程序中可以選擇捕獲處理,也可以不處理。雖然 Java 編譯器不會檢查運行時異常,但是我們也可以通過 throws 進行聲明拋出,也可以通過 try-catch 對它進行捕獲處理。如果產生運行時異常,則需要通過修改代碼來進行避免。例如,若會發生除數爲零的情況,則需要通過代碼避免該情況的發生!
RuntimeException 異常會由 Java 虛擬機自動拋出並自動捕獲(就算我們沒寫異常捕獲語句運行時也會拋出錯誤!!),此類異常的出現絕大數情況是代碼本身有問題應該從邏輯上去解決並改進代碼。
編譯時異常
定義: Exception 中除 RuntimeException 及其子類之外的異常。
特點: Java 編譯器會檢查它。如果程序中出現此類異常,比如 ClassNotFoundException(沒有找到指定的類異常),IOException(IO 流異常),要麼通過 throws 進行聲明拋出,要麼通過 try-catch 進行捕獲處理,否則不能通過編譯。在程序中,通常不會自定義該類異常,而是直接使用系統提供的異常類。該異常我們必須手動在代碼裏添加捕獲語句來處理該異常。
4. 受檢異常與非受檢異常
Java 的所有異常可以分爲受檢異常(checked exception)和非受檢異常(unchecked exception)。
受檢異常
編譯器要求必須處理的異常。正確的程序在運行過程中,經常容易出現的、符合預期的異常情況。一旦發生此類異常,就必須採用某種方式進行處理。除 RuntimeException 及其子類外,其他的 Exception 異常都屬於受檢異常。編譯器會檢查此類異常,也就是說當編譯器檢查到應用中的某處可能會此類異常時,將會提示你處理本異常——要麼使用 try-catch 捕獲,要麼使用方法簽名中用 throws 關鍵字拋出,否則編譯不通過。
非受檢異常
編譯器不會進行檢查並且不要求必須處理的異常,也就說當程序中出現此類異常時,即使我們沒有 try-catch 捕獲它,也沒有使用 throws 拋出該異常,編譯也會正常通過。該類異常包括運行時異常(RuntimeException 極其子類)和錯誤(Error)。
Java 異常關鍵字
-
try – 用於監聽。將要被監聽的代碼 (可能拋出異常的代碼) 放在 try 語句塊之內,當 try 語句塊內發生異常時,異常就被拋出。
-
catch – 用於捕獲異常。catch 用來捕獲 try 語句塊中發生的異常。
-
finally – finally 語句塊總是會被執行。它主要用於回收在 try 塊裏打開的物力資源 (如數據庫連接、網絡連接和磁盤文件)。只有 finally 塊,執行完成之後,纔會回來執行 try 或者 catch 塊中的 return 或者 throw 語句,如果 finally 中使用了 return 或者 throw 等終止方法的語句,則就不會跳回執行,直接停止。
-
throw – 用於拋出異常。
-
throws – 用在方法簽名中,用於聲明該方法可能拋出的異常。
Java 異常處理
Java 通過面向對象的方法進行異常處理,一旦方法拋出異常,系統自動根據該異常對象尋找合適異常處理器(Exception Handler)來處理該異常,把各種不同的異常進行分類,並提供了良好的接口。
在 Java 中,每個異常都是一個對象,它是 Throwable 類或其子類的實例。當一個方法出現異常後便拋出一個異常對象,該對象中包含有異常信息,調用這個對象的方法可以捕獲到這個異常並可以對其進行處理。Java 的異常處理是通過 5 個關鍵詞來實現的:try、catch、throw、throws 和 finally。
在 Java 應用中,異常的處理機制分爲聲明異常,拋出異常和捕獲異常。
聲明異常
通常,應該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續傳遞下去。傳遞異常可以在方法簽名處使用 throws 關鍵字聲明可能會拋出的異常。
注意
-
非檢查異常(Error、RuntimeException 或它們的子類)不可使用 throws 關鍵字來聲明要拋出的異常。
-
一個方法出現編譯時異常,就需要 try-catch/ throws 處理,否則會導致編譯錯誤。拋出異常
如果你覺得解決不了某些異常問題,且不需要調用者處理,那麼你可以拋出異常。
throw 關鍵字作用是在方法內部拋出一個 Throwable 類型的異常。任何 Java 代碼都可以通過 throw 語句拋出異常。
捕獲異常
程序通常在運行之前不報錯,但是運行後可能會出現某些未知的錯誤,但是還不想直接拋出到上一級,那麼就需要通過 try…catch… 的形式進行異常捕獲,之後根據不同的異常情況來進行相應的處理。
如何選擇異常類型
可以根據下圖來選擇是捕獲異常,聲明異常還是拋出異常
常見異常處理方式
直接拋出異常
通常,應該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續傳遞下去。傳遞異常可以在方法簽名處使用 throws 關鍵字聲明可能會拋出的異常。
private static void readFile(String filePath) throws IOException {
File file = new File(filePath);
String result;
BufferedReader reader = new BufferedReader(new FileReader(file));
while((result = reader.readLine())!=null) {
System.out.println(result);
}
reader.close();
}
封裝異常再拋出
有時我們會從 catch 中拋出一個異常,目的是爲了改變異常的類型。多用於在多系統集成時,當某個子系統故障,異常類型可能有多種,可以用統一的異常類型向外暴露,不需暴露太多內部異常細節。
private static void readFile(String filePath) throws MyException {
try {
// code
} catch (IOException e) {
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
}
}
捕獲異常
在一個 try-catch 語句塊中可以捕獲多個異常類型,並對不同類型的異常做出不同的處理
private static void readFile(String filePath) {
try {
// code
} catch (FileNotFoundException e) {
// handle FileNotFoundException
} catch (IOException e){
// handle IOException
}
}
同一個 catch 也可以捕獲多種類型異常,用 | 隔開
private static void readFile(String filePath) {
try {
// code
} catch (FileNotFoundException | UnknownHostException e) {
// handle FileNotFoundException or UnknownHostException
} catch (IOException e){
// handle IOException
}
}
自定義異常
習慣上,定義一個異常類應包含兩個構造函數,一個無參構造函數和一個帶有詳細描述信息的構造函數(Throwable 的 toString 方法會打印這些詳細信息,調試時很有用)
public class MyException extends Exception {
public MyException(){ }
public MyException(String msg){
super(msg);
}
// ...
}
try-catch-finally
當方法中發生異常,異常處之後的代碼不會再執行,如果之前獲取了一些本地資源需要釋放,則需要在方法正常結束時和 catch 語句中都調用釋放本地資源的代碼,顯得代碼比較繁瑣,finally 語句可以解決這個問題。
private static void readFile(String filePath) throws MyException {
File file = new File(filePath);
String result;
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
while((result = reader.readLine())!=null) {
System.out.println(result);
}
} catch (IOException e) {
System.out.println("readFile method catch block.");
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
} finally {
System.out.println("readFile method finally block.");
if (null != reader) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
調用該方法時,讀取文件時若發生異常,代碼會進入 catch 代碼塊,之後進入 finally 代碼塊;若讀取文件時未發生異常,則會跳過 catch 代碼塊直接進入 finally 代碼塊。所以無論代碼中是否發生異常,fianlly 中的代碼都會執行。
若 catch 代碼塊中包含 return 語句,finally 中的代碼還會執行嗎?將以上代碼中的 catch 子句修改如下:
catch (IOException e) {
System.out.println("readFile method catch block.");
return;
}
調用 readFile 方法,觀察當 catch 子句中調用 return 語句時,finally 子句是否執行
readFile method catch block.
readFile method finally block.
可見,即使 catch 中包含了 return 語句,finally 子句依然會執行。若 finally 中也包含 return 語句,finally 中的 return 會覆蓋前面的 return.
try-with-resource
上面例子中,finally 中的 close 方法也可能拋出 IOException, 從而覆蓋了原始異常。JAVA 7 提供了更優雅的方式來實現資源的自動釋放,自動釋放的資源需要是實現了 AutoCloseable 接口的類。
private static void tryWithResourceTest(){
try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
// code
} catch (IOException e){
// handle exception
}
}
try 代碼塊退出時,會自動調用 scanner.close 方法,和把 scanner.close 方法放在 finally 代碼塊中不同的是,若 scanner.close 拋出異常,則會被抑制,拋出的仍然爲原始異常。被抑制的異常會由 addSusppressed 方法添加到原來的異常,如果想要獲取被抑制的異常列表,可以調用 getSuppressed 方法來獲取。
Java 異常常見面試題
1. Error 和 Exception 區別是什麼?
Error 類型的錯誤通常爲虛擬機相關錯誤,如系統崩潰,內存不足,堆棧溢出等,編譯器不會對這類錯誤進行檢測,JAVA 應用程序也不應對這類錯誤進行捕獲,一旦這類錯誤發生,通常應用程序會被終止,僅靠應用程序本身無法恢復;
Exception 類的錯誤是可以在應用程序中進行捕獲並處理的,通常遇到這種錯誤,應對其進行處理,使應用程序可以繼續正常運行。
2. 運行時異常和一般異常 (受檢異常) 區別是什麼?
運行時異常包括 RuntimeException 類及其子類,表示 JVM 在運行期間可能出現的異常。Java 編譯器不會檢查運行時異常。
受檢異常是 Exception 中除 RuntimeException 及其子類之外的異常。Java 編譯器會檢查受檢異常。
RuntimeException 異常和受檢異常之間的區別:是否強制要求調用者必須處理此異常,如果強制要求調用者必須進行處理,那麼就使用受檢異常,否則就選擇非受檢異常 (RuntimeException)。一般來講,如果沒有特殊的要求,我們建議使用 RuntimeException 異常。
3. JVM 是如何處理異常的?
在一個方法中如果發生異常,這個方法會創建一個異常對象,並轉交給 JVM,該異常對象包含異常名稱,異常描述以及異常發生時應用程序的狀態。創建異常對象並轉交給 JVM 的過程稱爲拋出異常。可能有一系列的方法調用,最終才進入拋出異常的方法,這一系列方法調用的有序列表叫做調用棧。
JVM 會順着調用棧去查找看是否有可以處理異常的代碼,如果有,則調用異常處理代碼。當 JVM 發現可以處理異常的代碼時,會把發生的異常傳遞給它。如果 JVM 沒有找到可以處理該異常的代碼塊,JVM 就會將該異常轉交給默認的異常處理器(默認處理器爲 JVM 的一部分),默認異常處理器打印出異常信息並終止應用程序。
4. throw 和 throws 的區別是什麼?
Java 中的異常處理除了包括捕獲異常和處理異常之外,還包括聲明異常和拋出異常,可以通過 throws 關鍵字在方法上聲明該方法要拋出的異常,或者在方法內部通過 throw 拋出異常對象。
throws 關鍵字和 throw 關鍵字在使用上的幾點區別如下:
-
throw 關鍵字用在方法內部,只能用於拋出一種異常,用來拋出方法或代碼塊中的異常,受查異常和非受查異常都可以被拋出。
-
throws 關鍵字用在方法聲明上,可以拋出多個異常,用來標識該方法可能拋出的異常列表。一個方法用 throws 標識了可能拋出的異常列表,調用該方法的方法中必須包含可處理異常的代碼,否則也要在方法簽名中用 throws 關鍵字聲明相應的異常。
5. final、finally、finalize 有什麼區別?
-
final 可以修飾類、變量、方法,修飾類表示該類不能被繼承、修飾方法表示該方法不能被重寫、修飾變量表示該變量是一個常量不能被重新賦值。
-
finally 一般作用在 try-catch 代碼塊中,在處理異常的時候,通常我們將一定要執行的代碼方法 finally 代碼塊中,表示不管是否出現異常,該代碼塊都會執行,一般用來存放一些關閉資源的代碼。
-
finalize 是一個方法,屬於 Object 類的一個方法,而 Object 類是所有類的父類,Java 中允許使用 finalize() 方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。
6. NoClassDefFoundError 和 ClassNotFoundException 區別?
NoClassDefFoundError 是一個 Error 類型的異常,是由 JVM 引起的,不應該嘗試捕獲這個異常。
引起該異常的原因是 JVM 或 ClassLoader 嘗試加載某類時在內存中找不到該類的定義,該動作發生在運行期間,即編譯時該類存在,但是在運行時卻找不到了,可能是變異後被刪除了等原因導致;
ClassNotFoundException 是一個受查異常,需要顯式地使用 try-catch 對其進行捕獲和處理,或在方法簽名中用 throws 關鍵字進行聲明。當使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 動態加載類到內存的時候,通過傳入的類路徑參數沒有找到該類,就會拋出該異常;另一種拋出該異常的可能原因是某個類已經由一個類加載器加載至內存中,另一個加載器又嘗試去加載它。
7. try-catch-finally 中哪個部分可以省略?
答:catch 可以省略
原因
更爲嚴格的說法其實是:try 只適合處理運行時異常,try+catch 適合處理運行時異常 + 普通異常。也就是說,如果你只用 try 去處理普通異常卻不加以 catch 處理,編譯是通不過的,因爲編譯器硬性規定,普通異常如果選擇捕獲,則必須用 catch 顯示聲明以便進一步處理。而運行時異常在編譯時沒有如此規定,所以 catch 可以省略,你加上 catch 編譯器也覺得無可厚非。
理論上,編譯器看任何代碼都不順眼,都覺得可能有潛在的問題,所以你即使對所有代碼加上 try,代碼在運行期時也只不過是在正常運行的基礎上加一層皮。但是你一旦對一段代碼加上 try,就等於顯示地承諾編譯器,對這段代碼可能拋出的異常進行捕獲而非向上拋出處理。如果是普通異常,編譯器要求必須用 catch 捕獲以便進一步處理;如果運行時異常,捕獲然後丟棄並且 + finally 掃尾處理,或者加上 catch 捕獲以便進一步處理。
至於加上 finally,則是在不管有沒捕獲異常,都要進行的 “掃尾” 處理。
8. try-catch-finally 中,如果 catch 中 return 了,finally 還會執行嗎?
答:會執行,在 return 前執行。
注意:在 finally 中改變返回值的做法是不好的,因爲如果存在 finally 代碼塊,try 中的 return 語句不會立馬返回調用者,而是記錄下返回值待 finally 代碼塊執行完畢之後再向調用者返回其值,然後如果在 finally 中修改了返回值,就會返回修改後的值。顯然,在 finally 中返回或者修改返回值會對程序造成很大的困擾,C# 中直接用編譯錯誤的方式來阻止程序員幹這種齷齪的事情,Java 中也可以通過提升編譯器的語法檢查級別來產生警告或錯誤。
代碼示例 1:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序執行到這一步的時候,這裏不是return a 而是 return 30;這個返回路徑就形成了
* 但是呢,它發現後面還有finally,所以繼續執行finally的內容,a=40
* 再次回到以前的路徑,繼續走return 30,形成返回路徑之後,這裏的a就不是a變量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
執行結果:30
代碼示例 2:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
} finally {
a = 40;
//如果這樣,就又重新形成了一條返回路徑,由於只能通過1個return返回,所以這裏直接返回40
return a;
}
}
執行結果:40
9. 類 ExampleA 繼承 Exception,類 ExampleB 繼承 ExampleA。
有如下代碼片斷:
try {
throw new ExampleB("b")
} catch(ExampleA e){
System.out.println("ExampleA");
} catch(Exception e){
System.out.println("Exception");
}
請問執行此段代碼的輸出是什麼?
答:
輸出:ExampleA。(根據里氏代換原則 [能使用父類型的地方一定能使用子類型],抓取 ExampleA 類型異常的 catch 塊能夠抓住 try 塊中拋出的 ExampleB 類型的異常)
面試題 - 說出下面代碼的運行結果。(此題的出處是《Java 編程思想》一書)
class Annoyance extends Exception {
}
class Sneeze extends Annoyance {
}
class Human {
public static void main(String[] args)
throws Exception {
try {
try {
throw new Sneeze();
} catch ( Annoyance a ) {
System.out.println("Caught Annoyance");
throw a;
}
} catch ( Sneeze s ) {
System.out.println("Caught Sneeze");
return ;
} finally {
System.out.println("Hello World!");
}
}
}
結果
Caught Annoyance
Caught Sneeze
Hello World!
10. 常見的 RuntimeException 有哪些?
-
ClassCastException(類轉換異常)
-
IndexOutOfBoundsException(數組越界)
-
NullPointerException(空指針)
-
ArrayStoreException(數據存儲異常,操作數組時類型不一致)
-
還有 IO 操作的 BufferOverflowException 異常
11. Java 常見異常有哪些
java.lang.IllegalAccessError:違法訪問錯誤。當一個應用試圖訪問、修改某個類的域(Field)或者調用其方法,但是又違反域或方法的可見性聲明,則拋出該異常。
java.lang.InstantiationError:實例化錯誤。當一個應用試圖通過 Java 的 new 操作符構造一個抽象類或者接口時拋出該異常.
java.lang.OutOfMemoryError:內存不足錯誤。當可用內存不足以讓 Java 虛擬機分配給一個對象時拋出該錯誤。
java.lang.StackOverflowError:堆棧溢出錯誤。當一個應用遞歸調用的層次太深而導致堆棧溢出或者陷入死循環時拋出該錯誤。
java.lang.ClassCastException:類造型異常。假設有類 A 和 B(A 不是 B 的父類或子類),O 是 A 的實例,那麼當強制將 O 構造爲類 B 的實例時拋出該異常。該異常經常被稱爲強制類型轉換異常。
java.lang.ClassNotFoundException:找不到類異常。當應用試圖根據字符串形式的類名構造類,而在遍歷 CLASSPAH 之後找不到對應名稱的 class 文件時,拋出該異常。
java.lang.ArithmeticException:算術條件異常。譬如:整數除零等。
java.lang.ArrayIndexOutOfBoundsException:數組索引越界異常。當對數組的索引值爲負數或大於等於數組大小時拋出。
java.lang.IndexOutOfBoundsException:索引越界異常。當訪問某個序列的索引值小於 0 或大於等於序列大小時,拋出該異常。
java.lang.InstantiationException:實例化異常。當試圖通過 newInstance() 方法創建某個類的實例,而該類是一個抽象類或接口時,拋出該異常。
java.lang.NoSuchFieldException:屬性不存在異常。當訪問某個類的不存在的屬性時拋出該異常。
java.lang.NoSuchMethodException:方法不存在異常。當訪問某個類的不存在的方法時拋出該異常。
java.lang.NullPointerException:空指針異常。當應用試圖在要求使用對象的地方使用了 null 時,拋出該異常。譬如:調用 null 對象的實例方法、訪問 null 對象的屬性、計算 null 對象的長度、使用 throw 語句拋出 null 等等。
java.lang.NumberFormatException:數字格式異常。當試圖將一個 String 轉換爲指定的數字類型,而該字符串確不滿足數字類型要求的格式時,拋出該異常。
java.lang.StringIndexOutOfBoundsException:字符串索引越界異常。當使用索引值訪問某個字符串中的字符,而該索引值小於 0 或大於等於序列大小時,拋出該異常。
Java 異常處理最佳實踐
在 Java 中處理異常並不是一個簡單的事情。不僅僅初學者很難理解,即使一些有經驗的開發者也需要花費很多時間來思考如何處理異常,包括需要處理哪些異常,怎樣處理等等。這也是絕大多數開發團隊都會制定一些規則來規範進行異常處理的原因。而團隊之間的這些規範往往是截然不同的。
本文給出幾個被很多團隊使用的異常處理最佳實踐。
1. 在 finally 塊中清理資源或者使用 try-with-resource 語句
當使用類似 InputStream 這種需要使用後關閉的資源時,一個常見的錯誤就是在 try 塊的最後關閉資源。
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
// do NOT do this
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
問題就是,只有沒有異常拋出的時候,這段代碼纔可以正常工作。try 代碼塊內代碼會正常執行,並且資源可以正常關閉。但是,使用 try 代碼塊是有原因的,一般調用一個或多個可能拋出異常的方法,而且,你自己也可能會拋出一個異常,這意味着代碼可能不會執行到 try 代碼塊的最後部分。結果就是,你並沒有關閉資源。
所以,你應該把清理工作的代碼放到 finally 裏去,或者使用 try-with-resource 特性。
1.1 使用 finally 代碼塊
與前面幾行 try 代碼塊不同,finally 代碼塊總是會被執行。不管 try 代碼塊成功執行之後還是你在 catch 代碼塊中處理完異常後都會執行。因此,你可以確保你清理了所有打開的資源。
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
1.2 Java 7 的 try-with-resource 語法
如果你的資源實現了 AutoCloseable 接口,你可以使用這個語法。大多數的 Java 標準資源都繼承了這個接口。當你在 try 子句中打開資源,資源會在 try 代碼塊執行後或異常處理後自動關閉。
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
2. 優先明確的異常
你拋出的異常越明確越好,永遠記住,你的同事或者幾個月之後的你,將會調用你的方法並且處理異常。
因此需要保證提供給他們儘可能多的信息。這樣你的 API 更容易被理解。你的方法的調用者能夠更好的處理異常並且避免額外的檢查。
因此,總是嘗試尋找最適合你的異常事件的類,例如,拋出一個 NumberFormatException 來替換一個 IllegalArgumentException 。避免拋出一個不明確的異常。
public void doNotDoThis() throws Exception {
...
}
public void doThis() throws NumberFormatException {
...
}
3. 對異常進行文檔說明
當在方法上聲明拋出異常時,也需要進行文檔說明。目的是爲了給調用者提供儘可能多的信息,從而可以更好地避免或處理異常。
在 Javadoc 添加 @throws 聲明,並且描述拋出異常的場景。
public void doSomething(String input) throws MyBusinessException {
...
}
4. 使用描述性消息拋出異常
在拋出異常時,需要儘可能精確地描述問題和相關信息,這樣無論是打印到日誌中還是在監控工具中,都能夠更容易被人閱讀,從而可以更好地定位具體錯誤信息、錯誤的嚴重程度等。
但這裏並不是說要對錯誤信息長篇大論,因爲本來 Exception 的類名就能夠反映錯誤的原因,因此只需要用一到兩句話描述即可。
如果拋出一個特定的異常,它的類名很可能已經描述了這種錯誤。所以,你不需要提供很多額外的信息。一個很好的例子是 NumberFormatException 。當你以錯誤的格式提供 String 時,它將被 java.lang.Long 類的構造函數拋出。
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}
5. 優先捕獲最具體的異常
大多數 IDE 都可以幫助你實現這個最佳實踐。當你嘗試首先捕獲較不具體的異常時,它們會報告無法訪問的代碼塊。
但問題在於,只有匹配異常的第一個 catch 塊會被執行。因此,如果首先捕獲 IllegalArgumentException ,則永遠不會到達應該處理更具體的 NumberFormatException 的 catch 塊,因爲它是 IllegalArgumentException 的子類。
總是優先捕獲最具體的異常類,並將不太具體的 catch 塊添加到列表的末尾。
你可以在下面的代碼片斷中看到這樣一個 try-catch 語句的例子。第一個 catch 塊處理所有 NumberFormatException 異常,第二個處理所有非 NumberFormatException 異常的 IllegalArgumentException 異常。
public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
6. 不要捕獲 Throwable 類
Throwable 是所有異常和錯誤的超類。你可以在 catch 子句中使用它,但是你永遠不應該這樣做!
如果在 catch 子句中使用 Throwable ,它不僅會捕獲所有異常,也將捕獲所有的錯誤。JVM 拋出錯誤,指出不應該由應用程序處理的嚴重問題。典型的例子是 OutOfMemoryError 或者 StackOverflowError 。兩者都是由應用程序控制之外的情況引起的,無法處理。
所以,最好不要捕獲 Throwable ,除非你確定自己處於一種特殊的情況下能夠處理錯誤。
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
7. 不要忽略異常
很多時候,開發者很有自信不會拋出異常,因此寫了一個 catch 塊,但是沒有做任何處理或者記錄日誌。
public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen
}
}
但現實是經常會出現無法預料的異常,或者無法確定這裏的代碼未來是不是會改動 (刪除了阻止異常拋出的代碼),而此時由於異常被捕獲,使得無法拿到足夠的錯誤信息來定位問題。
合理的做法是至少要記錄異常的信息。
public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e);
}
}
8. 不要記錄並拋出異常
這可能是本文中最常被忽略的最佳實踐。可以發現很多代碼甚至類庫中都會有捕獲異常、記錄日誌並再次拋出的邏輯。如下:
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
這個處理邏輯看着是合理的。但這經常會給同一個異常輸出多條日誌。如下:
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
如上所示,後面的日誌也沒有附加更有用的信息。如果想要提供更加有用的信息,那麼可以將異常包裝爲自定義異常。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
因此,僅僅當想要處理異常時纔去捕獲,否則只需要在方法簽名中聲明讓調用者去處理。
9. 包裝異常時不要拋棄原始的異常
捕獲標準異常幷包裝爲自定義異常是一個很常見的做法。這樣可以添加更爲具體的異常信息並能夠做針對的異常處理。
在你這樣做時,請確保將原始異常設置爲原因(注:參考下方代碼 NumberFormatException e 中的原始異常 e )。Exception 類提供了特殊的構造函數方法,它接受一個 Throwable 作爲參數。否則,你將會丟失堆棧跟蹤和原始異常的消息,這將會使分析導致異常的異常事件變得困難。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
10. 不要使用異常控制程序的流程
不應該使用異常控制應用的執行流程,例如,本應該使用 if 語句進行條件判斷的情況下,你卻使用異常處理,這是非常不好的習慣,會嚴重影響應用的性能。
11. 使用標準異常
如果使用內建的異常可以解決問題,就不要定義自己的異常。Java API 提供了上百種針對不同情況的異常類型,在開發中首先儘可能使用 Java API 提供的異常,如果標準的異常不能滿足你的要求,這時候創建自己的定製異常。儘可能得使用標準異常有利於新加入的開發者看懂項目代碼。
12. 異常會影響性能
異常處理的性能成本非常高,每個 Java 程序員在開發時都應牢記這句話。創建一個異常非常慢,拋出一個異常又會消耗 1~5ms,當一個異常在應用的多個層級之間傳遞時,會拖累整個應用的性能。
僅在異常情況下使用異常;
-
在可恢復的異常情況下使用異常;
-
儘管使用異常有利於 Java 開發,但是在應用中最好不要捕獲太多的調用棧,因爲在很多情況下都不需要打印調用棧就知道哪裏出錯了。因此,異常消息應該提供恰到好處的信息。
13. 總結
綜上所述,當你拋出或捕獲異常的時候,有很多不同的情況需要考慮,而且大部分事情都是爲了改善代碼的可讀性或者 API 的可用性。
異常不僅僅是一個錯誤控制機制,也是一個通信媒介。因此,爲了和同事更好的合作,一個團隊必須要制定出一個最佳實踐和規則,只有這樣,團隊成員才能理解這些通用概念,同時在工作中使用它。
異常處理 - 阿里巴巴 Java 開發手冊【強制】Java 類庫中定義的可以通過預檢查方式規避的 RuntimeException 異常不應該通過 catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException 等等。說明:無法通過預檢查的異常除外,比如,在解析字符串形式的數字時,可能存在數字格式錯誤,不得不通過 catch NumberFormatException 來實現。正例:if (obj != null) {…} 反例:try { obj.method(); } catch (NullPointerException e) {…}
【強制】異常不要用來做流程控制,條件控制。說明:異常設計的初衷是解決程序運行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。
【強制】catch 時請分清穩定代碼和非穩定代碼,穩定代碼指的是無論如何不會出錯的代碼。對於非穩定代碼的 catch 儘可能進行區分異常類型,再做對應的異常處理。說明:對大段代碼進行 try-catch,使程序無法根據不同的異常做出正確的應激反應,也不利於定位問題,這是一種不負責任的表現。正例:用戶註冊的場景中,如果用戶輸入非法字符,或用戶名稱已存在,或用戶輸入密碼過於簡單,在程序上作出分門別類的判斷,並提示給用戶。
【強制】捕獲異常是爲了處理它,不要捕獲了卻什麼都不處理而拋棄之,如果不想處理它,請將該異常拋給它的調用者。最外層的業務使用者,必須處理異常,將其轉化爲用戶可以理解的內容。
【強制】有 try 塊放到了事務代碼中,catch 異常後,如果需要回滾事務,一定要注意手動回滾事務。
【強制】finally 塊必須對資源對象、流對象進行關閉,有異常也要做 try-catch。說明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
【強制】不要在 finally 塊中使用 return。說明:try 塊中的 return 語句執行成功後,並不馬上返回,而是繼續執行 finally 塊中的語句,如果此處存在 return 語句,則在此直接返回,無情丟棄掉 try 塊中的返回點。反例:
private int x = 0;
public int checkReturn() {
try {
// x等於1,此處不返回
return ++x;
} finally {
// 返回的結果是2
return ++x;
}
}
【強制】捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。說明:如果預期對方拋的是繡球,實際接到的是鉛球,就會產生意外情況。
【強制】在調用 RPC、二方包、或動態生成類的相關方法時,捕捉異常必須使用 Throwable 類來進行攔截。說明:通過反射機制來調用方法,如果找不到方法,拋出 NoSuchMethodException。什麼情況會拋出 NoSuchMethodError 呢?二方包在類衝突時,仲裁機制可能導致引入非預期的版本使類的方法簽名不匹配,或者在字節碼修改框架(比如:ASM)動態創建或修改類時,修改了相應的方法簽名。這些情況,即使代碼編譯期是正確的,但在代碼運行期時,會拋出 NoSuchMethodError。
【推薦】方法的返回值可以爲 null,不強制返回空集合,或者空對象等,必須添加註釋充分說明什麼情況下會返回 null 值。說明:本手冊明確防止 NPE 是調用者的責任。即使被調用方法返回空集合或者空對象,對調用者來說,也並非高枕無憂,必須考慮到遠程調用失敗、序列化失敗、運行時異常等場景返回 null 的情況。
【推薦】防止 NPE,是程序員的基本修養,注意 NPE 產生的場景:1) 返回類型爲基本數據類型,return 包裝數據類型的對象時,自動拆箱有可能產生 NPE。反例:public int f() { return Integer 對象}, 如果爲 null,自動解箱拋 NPE。2) 數據庫的查詢結果可能爲 null。3) 集合裏的元素即使 isNotEmpty,取出的數據元素也可能爲 null。4) 遠程調用返回對象時,一律要求進行空指針判斷,防止 NPE。5) 對於 Session 中獲取的數據,建議進行 NPE 檢查,避免空指針。6) 級聯調用 obj.getA().getB().getC();一連串調用,易產生 NPE。正例:使用 JDK8 的 Optional 類來防止 NPE 問題。
【推薦】定義時區分 unchecked / checked 異常,避免直接拋出 new RuntimeException(),更不允許拋出 Exception 或者 Throwable,應使用有業務含義的自定義異常。推薦業界已定義過的自定義異常,如:DAOException / ServiceException 等。
【參考】對於公司外的 http/api 開放接口必須使用 “錯誤碼”;而應用內部推薦異常拋出;跨應用間 RPC 調用優先考慮使用 Result 方式,封裝 isSuccess() 方法、“錯誤碼”、“錯誤簡短信息”。說明:關於 RPC 方法返回方式使用 Result 方式的理由:1)使用拋異常返回方式,調用方如果沒有捕獲到就會產生運行時錯誤。2)如果不加棧信息,只是 new 自定義異常,加入自己的理解的 error message,對於調用端解決問題的幫助不會太多。如果加了棧信息,在頻繁調用出錯的情況下,數據序列化和傳輸的性能損耗也是問題。
【參考】避免出現重複的代碼(Don’t Repeat Yourself),即 DRY 原則。說明:隨意複製和粘貼代碼,必然會導致代碼的重複,在以後需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是組件化。正例:一個類中有多個 public 方法,都需要進行數行相同的參數校驗操作,這個時候請抽取:private boolean checkParam(DTO dto) {…}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/SSX7-v3emcxsOvZuJ2_XUg