2020-10-08
最最最全面的Java異常面試及解答
作者 | ThinkWon
來源 | blog.csdn.net/ThinkWon/article/details/101681073
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) {…}