Java核心知識體系3:異常機制詳解
1 什麼是異常
異常是指程式在運行過程中發生的,由於外部問題導致的運行異常事件,如:文件找不到、網路連接失敗、空指針、非法參數等。
異常是一個事件,它發生在程式運行期間,且中斷程式的運行。
Java 是一種面向對象的程式語言,它的異常都是對象,是Throwable子類的實例,當程式中存在錯誤條件時,且條件生成時,錯誤就會引發異常。
2 異常的分類
要了解異常的分類,我們先看看Java異常類的繼承結構圖:
2.1 Throwable
Throwable 是 Java 語言中所有錯誤與異常的頂層父類,其他類都繼承於該類。 Throwable 包含兩個子類:Error(錯誤)和 Exception(異常),它們通常用於指示發生了異常情況。 Throwable 包含了其執行緒創建時執行緒執行堆棧的快照,它提供了 printStackTrace() 等介面用於獲取堆棧跟蹤數據等資訊。
2.2 Error(錯誤)
Error 類及其子類:程式中無法處理的錯誤,表示運行應用程式中出現了嚴重的錯誤。通常情況為下應用程式 “不應該試圖捕獲的嚴重問題” 。
此類錯誤一般表示程式碼運行時 JVM 出現問題。通常有 Virtual MachineError(虛擬機運行錯誤)、NoClassDefFoundError(類定義錯誤)等。比如 OutOfMemoryError:記憶體不足錯誤;StackOverflowError:棧溢出錯誤。這些錯誤是不可查的,因為它們在應用程式的控制和處理能力之 外,而且絕大多數是程式運行時不允許出現的狀況。在 Java中,錯誤通過Error的子類描述。
2.3 Exception(異常)
Exception以及它的子類,代表程式運行時發送的各種不期望發生的事件。可以被Java異常處理機制使用,是異常處理的核心。Exception 這種異常又分為兩類:運行時異常和編譯時異常。
2.3.1 運行時異常
都是RuntimeException類及其子類異常,如NullPointerException(空指針異常)、IndexOutOfBoundsException(下標越界異常)等,這些異常是不檢查異常,程式中可以選擇捕獲處理,也可以不處理。這些異常一般是由程式邏輯錯誤引起的,程式應該從邏輯角度儘可能避免這類異常的發生。 運行時異常的特點是Java編譯器不會檢查它,也就是說,當程式中可能出現這類異常,即使沒有用try-catch語句捕獲它,也沒有用throws子句聲明拋出它,也會編譯通過。
2.3.2 非運行時異常 (編譯異常)
是RuntimeException以外的異常,類型上都屬於Exception類及其子類。從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如IOException、SQLException等以及用戶自定義的Exception異常,一般情況下不自定義檢查異常。
2.3.3 檢查性異常(checked exception)
正確的程式在運行中,很容易出現的、情理可容的異常狀況。可查異常雖然是異常狀況,但在一定程度上它的發生是可以預計的,而且一旦發生這種異常狀況,就必須採取某種方式進行處理。
除了Error 和 RuntimeException的其它異常。Java語言強制要求程式設計師為這樣的異常做預備處理工作(使用try…catch…finally或者throws)。在方法中要麼用try-catch語句捕獲它並處理,要麼用throws子句聲明拋出它,否則編譯不會通過。類似如SQLException,IOException,ClassNotFoundException 等。
常見的檢查性異常如下:
異常 | 描述 |
---|---|
Classnotfoundexception | 當應用程式試圖加數一個,通過名字查找時超發現沒有該的定義時,拋出該異常 |
Clonenotsupportedexcept | 當去克一個對象時,發現該對象沒有實現Cloneable介面時,拋出該異常 |
lllegalaccessexception | 當應用程式芸試通過反射的方式來訪間類、成員變數或調用方法時,卻無法訪問這些類、成員變數或方法的定義時,拋出該異常 |
Instantiationexception | 當試圖使用 Class:類中的 newinstance方法創建一個類的實例,而制定的類對象因為是一個介面或是一個抽象類而無法實例化時,拋出該異常 |
Interruptedexception | 個執行緒被另一個執行緒中斷時,拋出該異常 |
NosuchFieldexception | 當攏不到指定的變數欄位時,拋出該異常 |
NosuchMethodexception | 當我不到指定的類方法時,拋出該異常 |
2.3.4 非檢查性異常(checked exception)
包括運行時異常(RuntimeException與其子類)和錯誤(Error)及其子類。
Java語言在編譯時,不會提示和發現這樣的異常,不要求在程式中處理這些異常。所以我們可以在程式中編寫程式碼來處理(使用try…catch…finally)這樣的異常,也可以不做任何處理。
但是這種錯誤或異常,一般來說是程式邏輯錯誤導致的異常,所以我們應該修正程式碼,而不是通過異常處理器處理。
常見的非檢查性異常如下:
異常 | 描述 |
---|---|
Arithmeticexception | 當出現異常的運算條件時,拋出異常。例如,一個整數「除以零」時,拋出此美的一個實例 |
Arrayindexoutofboundsexcep | 用非法索引訪問數組時跑出的異常。如果索引為負或大於等於數組大小,則該索引為非法索引異常描述 |
Arraystoreexception | 試圖將錯誤類型的對象存儲到一個對象數組時,拋出的異常 |
Classcastexception | 試圖將對象強制轉換為不是同一個類型或其子類的實例時,拋出的異常 |
Illegalargumentexception | 當向一個方法傳遞非法或不正確的參數時,拋出該異常 |
IllegalmonitorstateException | 當某一執行緒已經試圖等待對象的監視器,或者通知其他正在等待該對象監視器的執行緒,而該執行緒本身沒有獲得指定監視器時拋出該異常 |
Illegalstateexception | 在非法或不適當的時間調用方法時產生的訊號。或者說Java環境或應用程式沒有處於請求操作所要求的適當狀態下 |
IllegalthreadstateException | 執行緒沒有處於請求操作所要求的適當狀態時,拋出該異常。 |
Indexoutofboundsexception | 當某種排序的索引超出範圍時拋出的異常,例如,一個數組,字元串或一個向量的排序等 |
Negativearraysizeexception | 如果應用程式試圖創建大小為負的數組時,拋出該異常 |
Nullpointerexception | 當應用程式在需要操作對象的時候而獲得的對象實例是nu時拋出該異常 |
Numberformatexception | 當應用程式試圖將字元串轉換成一種數值類型,但該字元串不能轉換為適當格式時,拋出該異常。 |
SecurityException | 由安全管理器拋出的異常,指示存在安全侵犯。 |
StringindexoutofboundsExcept | 此異常由 String方法拋出,說明索引為負或者超出了字元串的大小 |
3 異常基礎詳解
3.1 異常關鍵字
- try – 用於監聽。
將要被監聽的程式碼(可能拋出異常的程式碼)放在try語句塊之內,當try語句塊內發生異常時,異常就被拋出。 - catch – 用於捕獲異常。
catch用來捕獲try語句塊中發生的異常。 - finally – finally語句塊總是會被執行。
它主要用於回收在try塊里打開的物力資源(如資料庫連接、網路連接和磁碟文件)。只有finally塊,執行完成之後,才會回來執行try或者catch塊中的return或者throw語句,如果finally中使用了return或者throw等終止方法的語句,則就不會跳回執行,直接停止。 - throw – 用於拋出異常。
- throws – 用在方法簽名中,用於聲明該方法可能拋出的異常。
3.2 throws-異常的顯示聲明
在Java中,當前執行的語句必屬於某個方法,Java解釋器調用main方法執行開始執行程式。若方法中存在檢查異常,如果不對其捕獲,那必須在方法頭中顯式聲明該異常,以便於告知方法調用者此方法有異常,需要進行處理。 在方法中聲明一個異常,方法頭中使用關鍵字throws,後面接上要聲明的異常。若聲明多個異常,則使用逗號分割。如下所示:
public static void yourMethod() throws Exception{
//todo 業務邏輯
}
注意:若是父類的方法沒有聲明異常,則子類繼承方法後,也不能聲明異常。
通常,應該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續傳遞下去。傳遞異常可以在方法簽名處使用 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();
}
Throws拋出異常的規則:
- 如果是不可查異常(unchecked exception),即Error、RuntimeException或它們的子類,那麼可以不使用throws關鍵字來聲明要拋出的異常,編譯仍能順利通過,但在運行時會被系統拋出。
- 必須聲明方法可拋出的任何可查異常(checked exception)。即如果一個方法可能出現受可查異常,要麼用try-catch語句捕獲,要麼用throws子句聲明將它拋出,否則會導致編譯錯誤
- 僅當拋出了異常,該方法的調用者才必須處理或者重新拋出該異常。當方法的調用者無力處理該異常的時候,應該繼續拋出,而不是囫圇吞棗。
- 調用方法必須遵循任何可查異常的處理和聲明規則。若覆蓋一個方法,則不能聲明與覆蓋方法不同的異常。聲明的任何異常必須是被覆蓋方法所聲明異常的同類或子類。
3.3 throw-拋出異常
如果程式碼可能會引發某種錯誤,可以創建一個合適的異常類實例並拋出它,這就是拋出異常。如下所示:
public static double yourMethod(int value) {
if(value < 0) {
throw new ArithmeticException("參數不能為0"); //拋出一個運行時異常
}
return 6.0 / value;
}
大部分情況下都不需要手動拋出異常,因為Java的大部分方法要麼已經處理異常,要麼已聲明異常。所以一般都是捕獲異常或者再往上拋。
有時我們會從 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;
}
}
3.4 異常的自定義
習慣上,定義一個異常類應包含兩個構造函數,一個無參構造函數和一個帶有詳細描述資訊的構造函數(Throwable 的 toString 方法會列印這些詳細資訊,調試時很有用), 比如上面用到的自定義
MyException:
public class MyException extends Exception {
public MyException(){ }
public MyException(String msg){
super(msg);
}
// ...
}
3.5 異常的捕獲
異常捕獲處理的方法通常有:
- try-catch
- try-catch-finally
- try-finally
- try-with-resource
3.5.1 try-catch
在一個 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
}
}
3.5.2 try-catch-finally
- 常規語法
try {
//執行程式程式碼,可能會出現異常
} catch(Exception e) {
//捕獲異常並處理
} finally {
//必執行的程式碼
}
-
執行的順序
- 當try沒有捕獲到異常時:try語句塊中的語句逐一被執行,程式將跳過catch語句塊,執行finally語句塊和其後的語句;
- 當try捕獲到異常,catch語句塊里沒有處理此異常的情況:當try語句塊里的某條語句出現異常時,而沒有處理此異常的catch語句塊時,此異常將會拋給JVM處理,finally語句塊里的語句還是會被執行,但finally語句塊後的語句不會被執行;
- 當try捕獲到異常,catch語句塊里有處理此異常的情況:在try語句塊中是按照順序來執行的,當執行到某一條語句出現異常時,程式將跳到catch語句塊,並與catch語句塊逐一匹配,找到與之對應的處理程式,其他的catch語句塊將不會被執行,而try語句塊中,出現異常之後的語句也不會被執行,catch語句塊執行完後,執行finally語句塊里的語句,最後執行finally語句塊後的語句;
-
無異常情況 ,catch 模組被忽略,先執行業務邏輯,再執行finally。
-
異常情況,假設執行到業務邏輯2的時候,出現故障異常,則業務邏輯3沒有執行,直接執行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();
}
}
}
}
3.5.3 try-finally
也可以直接用try-finally嗎。try塊中引起異常,異常程式碼之後的語句不再執行,直接執行finally語句。
try塊沒有引發異常,則執行完try塊就執行finally語句。 try-finally可用在不需要捕獲異常的程式碼,可以保證資源在使用後被關閉。例如IO流中執行完相應操作後,關閉相應資源;使用Lock對象保證執行緒同步,通過finally可以保證鎖會被釋放;資料庫連接程式碼時,關閉連接操作等等。
//以Lock加鎖為例,演示try-finally
ReentrantLock lock = new ReentrantLock();
try {
//需要加鎖的程式碼
} finally {
lock.unlock(); //保證鎖一定被釋放
}
finally遇見如下情況不會執行
- 在前面的程式碼中用了System.exit()退出程式。
- finally語句塊中發生了異常。
- 程式所在的執行緒死亡。
- 關閉CPU。
3.5.4 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
}
}
- 看下Scanner
public final class Scanner implements Iterator<String>, Closeable {
// ...
}
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
try 程式碼塊退出時,會自動調用 scanner.close 方法,和把 scanner.close 方法放在 finally 程式碼塊中不同的是,若 scanner.close 拋出異常,則會被抑制,拋出的仍然為原始異常。被抑制的異常會由 addSusppressed 方法添加到原來的異常,如果想要獲取被抑制的異常列表,可以調用 getSuppressed 方法來獲取。
3.6 異常總結
- try、catch和finally都不能單獨使用,只能是try-catch、try-finally或者try-catch-finally。
- try語句塊監控程式碼,出現異常就停止執行下面的程式碼,然後將異常移交給catch語句塊來處理。
- finally語句塊中的程式碼一定會被執行,常用於回收資源 。
- throws:聲明一個異常,告知方法調用者。
- throw :拋出一個異常,至於該異常被捕獲還是繼續拋出都與它無關。
Java編程思想一書中,對異常的總結。 - 在恰當的級別處理問題。(在知道該如何處理的情況下了捕獲異常。)
- 解決問題並且重新調用產生異常的方法。
- 進行少許修補,然後繞過異常發生的地方繼續執行。
- 用別的數據進行計算,以代替方法預計會返回的值。
- 把當前運行環境下能做的事盡量做完,然後把相同的異常重拋到更高層。
- 把當前運行環境下能做的事盡量做完,然後把不同的異常拋到更高層。
- 終止程式。
- 進行簡化(如果你的異常模式使問題變得太複雜,那麼用起來會非常痛苦)。
- 讓類庫和程式更安全。
3.7 常用的異常
在Java中提供了一些異常用來描述經常發生的錯誤,對於這些異常,有的需要程式設計師進行捕獲處理或聲明拋出,有的是由Java虛擬機自動進行捕獲處理。Java中常見的異常類:
- RuntimeException
- java.lang.ArrayIndexOutOfBoundsException 數組索引越界異常。當對數組的索引值為負數或大於等於數組大小時拋出。
- java.lang.ArithmeticException 算術條件異常。譬如:整數除零等。
- java.lang.NullPointerException 空指針異常。當應用試圖在要求使用對象的地方使用了null時,拋出該異常。譬如:調用null對象的實例方法、訪問null對象的屬性、計算null對象的長度、使用throw語句拋出null等等
- java.lang.ClassNotFoundException 找不到類異常。當應用試圖根據字元串形式的類名構造類,而在遍歷CLASSPAH之後找不到對應名稱的class文件時,拋出該異常。
- java.lang.NegativeArraySizeException 數組長度為負異常
- java.lang.ArrayStoreException 數組中包含不兼容的值拋出的異常
- java.lang.SecurityException 安全性異常
- java.lang.IllegalArgumentException 非法參數異常
- IOException
- IOException:操作輸入流和輸出流時可能出現的異常。
- EOFException 文件已結束異常
- FileNotFoundException 文件未找到異常
- 其他
- ClassCastException 類型轉換異常類
- ArrayStoreException 數組中包含不兼容的值拋出的異常
- SQLException 操作資料庫異常類
- NoSuchFieldException 欄位未找到異常
- NoSuchMethodException 方法未找到拋出的異常
- NumberFormatException 字元串轉換為數字拋出的異常
- StringIndexOutOfBoundsException 字元串索引超出範圍拋出的異常
- IllegalAccessException 不允許訪問某類異常
- InstantiationException 當應用程式試圖使用Class類中的newInstance()方法創建一個類的實例,而指定的類對象無法被實例化時,拋出該異常
4 異常實踐
當你拋出或捕獲異常的時候,有很多不同的情況需要考慮,而且大部分事情都是為了改善程式碼的可讀性或者 API 的可用性。
異常不僅僅是一個錯誤控制機制,也是一個通訊媒介。因此,為了和同事更好的合作,一個團隊必須要制定出一個最佳實踐和規則,只有這樣,團隊成員才能理解這些通用概念,同時在工作中使用它。
這裡給出幾個被很多團隊使用的異常處理最佳實踐。
4.1 只針對不正常的情況才使用異常
異常只應該被用於不正常的條件,它們永遠不應該被用於正常的控制流。《阿里手冊》中:【強制】Java 類庫中定義的可以通過預檢查方式規避的RuntimeException異常不應該通過catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException等等。
比如,在解析字元串形式的數字時,可能存在數字格式錯誤,不得通過catch Exception來實現
- 程式碼1
if (obj != null) {
//...
}
- 程式碼2
try {
obj.method();
} catch (NullPointerException e) {
//...
}
主要原因有三點:
- 異常機制的設計初衷是用於不正常的情況,所以很少會會JVM實現試圖對它們的性能進行優化。所以,創建、拋出和捕獲異常的開銷是很昂貴的。
- 把程式碼放在try-catch中返回阻止了JVM實現本來可能要執行的某些特定的優化。
- 對數組進行遍歷的標準模式並不會導致冗餘的檢查,有些現代的JVM實現會將它們優化掉。
4.2 在 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 特性。
- 方法一:使用 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);
}
}
}
}
- 方法二: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);
}
}
4.3 盡量使用標準的異常
重用現有的異常有幾個好處:
- 它使得你的API更加易於學習和使用,因為它與程式設計師原來已經熟悉的習慣用法是一致的。
- 對於用到這些API的程式而言,它們的可讀性更好,因為它們不會充斥著程式設計師不熟悉的異常。
- 異常類越少,意味著記憶體佔用越小,並且轉載這些類的時間開銷也越小。
Java標準異常中有幾個是經常被使用的異常。如下表格:
| 異常 | 使用場合 |
| ———— | ———— |
|IllegalArgumentException | 參數的值不合適|
|IllegalStateException | 參數的狀態不合適 |
|NullPointerException | 在null被禁止的情況下參數值為null|
|IndexOutOfBoundsException | 下標越界 |
|ConcurrentModificationException | 在禁止並發修改的情況下,對象檢測到並發修改|
|UnsupportedOperationException | 對象不支援客戶請求的方法 |
雖然它們是Java平台庫迄今為止最常被重用的異常,但是,在許可的條件下,其它的異常也可以被重用。例如,如果你要實現諸如複數或者矩陣之類的算術對象,那麼重用ArithmeticException和NumberFormatException將是非常合適的。如果一個異常滿足你的需要,則不要猶豫,使用就可以,不過你一定要確保拋出異常的條件與該異常的文檔中描述的條件一致。這種重用必須建立在語義的基礎上,而不是名字的基礎上。
最後,一定要清楚,選擇重用哪一種異常並沒有必須遵循的規則。例如,考慮紙牌對象的情形,假設有一個用於發牌操作的方法,它的參數(handSize)是發一手牌的紙牌張數。假設調用者在這個參數中傳遞的值大於整副牌的剩餘張數。那麼這種情形既可以被解釋為IllegalArgumentException(handSize的值太大),也可以被解釋為IllegalStateException(相對客戶的請求而言,紙牌對象的紙牌太少)。
4.4 對異常進行文檔說明
當在方法上聲明拋出異常時,也需要進行文檔說明。目的是為了給調用者提供儘可能多的資訊,從而可以更好地避免或處理異常。
在 Javadoc 添加 @throws 聲明,並且描述拋出異常的場景。
/**
* Method description
*
* @throws MyBusinessException - businuess exception description
*/
public void doSomething(String input) throws MyBusinessException {
// ...
}
同時,在拋出MyBusinessException 異常時,需要儘可能精確地描述問題和相關資訊,這樣無論是列印到日誌中還是在監控工具中,都能夠更容易被人閱讀,從而可以更好地定位具體錯誤資訊、錯誤的嚴重程度等。
4.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)
}
}
4.6 不要捕獲 Throwable 類
Throwable 是所有異常和錯誤的超類。你可以在 catch 子句中使用它,但是你永遠不應該這樣做!
如果在 catch 子句中使用 Throwable ,它不僅會捕獲所有異常,也將捕獲所有的錯誤。JVM 拋出錯誤,指出不應該由應用程式處理的嚴重問題。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。兩者都是由應用程式控制之外的情況引起的,無法處理。
所以,最好不要捕獲 Throwable ,除非你確定自己處於一種特殊的情況下能夠處理錯誤。
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
4.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); // see this line
}
}
4.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);
}
}
因此,僅僅當想要處理異常時才去捕獲,否則只需要在方法簽名中聲明讓調用者去處理。
4.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);
}
}
4.10 不要使用異常控制程式的流程
不應該使用異常控制應用的執行流程,例如,本應該使用if語句進行條件判斷的情況下,你卻使用異常處理,這是非常不好的習慣,會嚴重影響應用的性能。
4.11 不要在finally塊中使用return。
try塊中的return語句執行成功後,並不馬上返回,而是繼續執行finally塊中的語句,如果此處存在return語句,則在此直接返回,無情丟棄掉try塊中的返回點。
如下是一個反例:
private int x = 0;
public int checkReturn() {
try {
// x等於1,此處不返回
return ++x;
} finally {
// 返回的結果是2
return ++x;
}
}
5 總結
這邊詳細介紹了異常的概念、原理,以及在應用中的一些小結。異常的能力是我們快速定位程式錯誤的重要手段之一,也是我們不斷優化程式,提高程式健壯性的依據,所以熟練掌握異常的使用是非常有必要的。