異常是指程式在執行過程中發生的,由於外部問題導致的執行異常事件,如:檔案找不到、網路連線失敗、空指標、非法引數等。
異常是一個事件,它發生在程式執行期間,且中斷程式的執行。
Java 是一種物件導向的程式語言,它的異常都是物件,是Throwable子類的範例,當程式中存在錯誤條件時,且條件生成時,錯誤就會引發異常。
要了解異常的分類,我們先看看Java異常類的繼承結構圖:
Throwable 是 Java 語言中所有錯誤與異常的頂層父類別,其他類都繼承於該類。 Throwable 包含兩個子類:Error(錯誤)和 Exception(異常),它們通常用於指示發生了異常情況。 Throwable 包含了其執行緒建立時執行緒執行堆疊的快照,它提供了 printStackTrace() 等介面用於獲取堆疊跟蹤資料等資訊。
Error 類及其子類:程式中無法處理的錯誤,表示執行應用程式中出現了嚴重的錯誤。通常情況為下應用程式 "不應該試圖捕獲的嚴重問題" 。
此類錯誤一般表示程式碼執行時 JVM 出現問題。通常有 Virtual MachineError(虛擬機器器執行錯誤)、NoClassDefFoundError(類定義錯誤)等。比如 OutOfMemoryError:記憶體不足錯誤;StackOverflowError:棧溢位錯誤。這些錯誤是不可查的,因為它們在應用程式的控制和處理能力之 外,而且絕大多數是程式執行時不允許出現的狀況。在 Java中,錯誤通過Error的子類描述。
Exception以及它的子類,代表程式執行時傳送的各種不期望發生的事件。可以被Java例外處理機制使用,是例外處理的核心。Exception 這種異常又分為兩類:執行時異常和編譯時異常。
都是RuntimeException類及其子類異常,如NullPointerException(空指標異常)、IndexOutOfBoundsException(下標越界異常)等,這些異常是不檢查異常,程式中可以選擇捕獲處理,也可以不處理。這些異常一般是由程式邏輯錯誤引起的,程式應該從邏輯角度儘可能避免這類異常的發生。 執行時異常的特點是Java編譯器不會檢查它,也就是說,當程式中可能出現這類異常,即使沒有用try-catch語句捕獲它,也沒有用throws子句宣告丟擲它,也會編譯通過。
是RuntimeException以外的異常,型別上都屬於Exception類及其子類。從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如IOException、SQLException等以及使用者自定義的Exception異常,一般情況下不自定義檢查異常。
正確的程式在執行中,很容易出現的、情理可容的異常狀況。可查異常雖然是異常狀況,但在一定程度上它的發生是可以預計的,而且一旦發生這種異常狀況,就必須採取某種方式進行處理。
除了Error 和 RuntimeException的其它異常。Java語言強制要求程式設計師為這樣的異常做預備處理工作(使用try…catch…finally或者throws)。在方法中要麼用try-catch語句捕獲它並處理,要麼用throws子句宣告丟擲它,否則編譯不會通過。類似如SQLException,IOException,ClassNotFoundException 等。
常見的檢查性異常如下:
異常 | 描述 |
---|---|
Classnotfoundexception | 當應用程式試圖加數一個,通過名字查詢時超發現沒有該的定義時,丟擲該異常 |
Clonenotsupportedexcept | 當去克一個物件時,發現該物件沒有實現Cloneable介面時,丟擲該異常 |
lllegalaccessexception | 當應用程式芸試通過反射的方式來訪間類、成員變數或呼叫方法時,卻無法存取這些類、成員變數或方法的定義時,丟擲該異常 |
Instantiationexception | 當試圖使用 Class:類中的 newinstance方法創建一個類的範例,而制定的類物件因為是一個介面或是一個抽象類而無法範例化時,丟擲該異常 |
Interruptedexception | 個執行緒被另一個執行緒中斷時,丟擲該異常 |
NosuchFieldexception | 當攏不到指定的變數欄位時,丟擲該異常 |
NosuchMethodexception | 當我不到指定的類方法時,丟擲該異常 |
包括執行時異常(RuntimeException與其子類)和錯誤(Error)及其子類。
Java語言在編譯時,不會提示和發現這樣的異常,不要求在程式中處理這些異常。所以我們可以在程式中編寫程式碼來處理(使用try…catch…finally)這樣的異常,也可以不做任何處理。
但是這種錯誤或異常,一般來說是程式邏輯錯誤導致的異常,所以我們應該修正程式碼,而不是通過例外處理器處理。
常見的非檢查性異常如下:
異常 | 描述 |
---|---|
Arithmeticexception | 當出現異常的運算條件時,丟擲異常。例如,一個整數「除以零」時,丟擲此美的一個範例 |
Arrayindexoutofboundsexcep | 用非法索引存取陣列時跑出的異常。如果索引為負或大於等於陣列大小,則該索引為非法索引異常描述 |
Arraystoreexception | 試圖將錯誤型別的物件儲存到一個物件陣列時,丟擲的異常 |
Classcastexception | 試圖將物件強制轉換為不是同一個型別或其子類的範例時,丟擲的異常 |
Illegalargumentexception | 當向一個方法傳遞非法或不正確的引數時,丟擲該異常 |
IllegalmonitorstateException | 當某一執行緒已經試圖等待物件的監視器,或者通知其他正在等待該物件監視器的執行緒,而該執行緒本身沒有獲得指定監視器時丟擲該異常 |
Illegalstateexception | 在非法或不適當的時間呼叫方法時產生的訊號。或者說Java環境或應用程式沒有處於請求操作所要求的適當狀態下 |
IllegalthreadstateException | 執行緒沒有處於請求操作所要求的適當狀態時,丟擲該異常。 |
Indexoutofboundsexception | 當某種排序的索引超出範圍時丟擲的異常,例如,一個陣列,字串或一個向量的排序等 |
Negativearraysizeexception | 如果應用程式試圖建立大小為負的陣列時,丟擲該異常 |
Nullpointerexception | 當應用程式在需要操作物件的時候而獲得的物件範例是nu時丟擲該異常 |
Numberformatexception | 當應用程式試圖將字串轉換成一種數值型別,但該字串不能轉換為適當格式時,丟擲該異常。 |
SecurityException | 由安全管理器丟擲的異常,指示存在安全侵犯。 |
StringindexoutofboundsExcept | 此異常由 String方法丟擲,說明索引為負或者超出了字串的大小 |
在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丟擲異常的規則:
如果程式碼可能會引發某種錯誤,可以建立一個合適的異常類範例並丟擲它,這就是丟擲異常。如下所示:
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;
}
}
習慣上,定義一個異常類應包含兩個建構函式,一個無參建構函式和一個帶有詳細描述資訊的建構函式(Throwable 的 toString 方法會列印這些詳細資訊,偵錯時很有用), 比如上面用到的自定義
MyException:
public class MyException extends Exception {
public MyException(){ }
public MyException(String msg){
super(msg);
}
// ...
}
異常捕獲處理的方法通常有:
在一個 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
}
}
try {
//執行程式程式碼,可能會出現異常
} catch(Exception e) {
//捕獲異常並處理
} 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();
}
}
}
}
也可以直接用try-finally嗎。try塊中引起異常,異常程式碼之後的語句不再執行,直接執行finally語句。
try塊沒有引發異常,則執行完try塊就執行finally語句。 try-finally可用在不需要捕獲異常的程式碼,可以保證資源在使用後被關閉。例如IO流中執行完相應操作後,關閉相應資源;使用Lock物件保證執行緒同步,通過finally可以保證鎖會被釋放;資料庫連執行緒式碼時,關閉連線操作等等。
//以Lock加鎖為例,演示try-finally
ReentrantLock lock = new ReentrantLock();
try {
//需要加鎖的程式碼
} finally {
lock.unlock(); //保證鎖一定被釋放
}
finally遇見如下情況不會執行
上面例子中,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
}
}
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 方法來獲取。
在Java中提供了一些異常用來描述經常發生的錯誤,對於這些異常,有的需要程式設計師進行捕獲處理或宣告丟擲,有的是由Java虛擬機器器自動進行捕獲處理。Java中常見的異常類:
當你丟擲或捕獲異常的時候,有很多不同的情況需要考慮,而且大部分事情都是為了改善程式碼的可讀性或者 API 的可用性。
異常不僅僅是一個錯誤控制機制,也是一個通訊媒介。因此,為了和同事更好的合作,一個團隊必須要制定出一個最佳實踐和規則,只有這樣,團隊成員才能理解這些通用概念,同時在工作中使用它。
這裡給出幾個被很多團隊使用的例外處理最佳實踐。
異常只應該被用於不正常的條件,它們永遠不應該被用於正常的控制流。《阿里手冊》中:【強制】Java 類庫中定義的可以通過預檢查方式規避的RuntimeException異常不應該通過catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException等等。
比如,在解析字串形式的數位時,可能存在數位格式錯誤,不得通過catch Exception來實現
if (obj != null) {
//...
}
try {
obj.method();
} catch (NullPointerException e) {
//...
}
主要原因有三點:
當使用類似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 特性。
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);
}
}
}
}
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);
}
}
重用現有的異常有幾個好處:
異常 | 使用場合 |
---|---|
IllegalArgumentException | 引數的值不合適 |
IllegalStateException | 引數的狀態不合適 |
NullPointerException | 在null被禁止的情況下引數值為null |
IndexOutOfBoundsException | 下標越界 |
ConcurrentModificationException | 在禁止並行修改的情況下,物件檢測到並行修改 |
UnsupportedOperationException | 物件不支援客戶請求的方法 |
雖然它們是Java平臺庫迄今為止最常被重用的異常,但是,在許可的條件下,其它的異常也可以被重用。例如,如果你要實現諸如複數或者矩陣之類的算術物件,那麼重用ArithmeticException和NumberFormatException將是非常合適的。如果一個異常滿足你的需要,則不要猶豫,使用就可以,不過你一定要確保丟擲異常的條件與該異常的檔案中描述的條件一致。這種重用必須建立在語意的基礎上,而不是名字的基礎上。
最後,一定要清楚,選擇重用哪一種異常並沒有必須遵循的規則。例如,考慮紙牌物件的情形,假設有一個用於發牌操作的方法,它的引數(handSize)是發一手牌的紙牌張數。假設呼叫者在這個引數中傳遞的值大於整副牌的剩餘張數。那麼這種情形既可以被解釋為IllegalArgumentException(handSize的值太大),也可以被解釋為IllegalStateException(相對客戶的請求而言,紙牌物件的紙牌太少)。
當在方法上宣告丟擲異常時,也需要進行檔案說明。目的是為了給呼叫者提供儘可能多的資訊,從而可以更好地避免或處理異常。
在 Javadoc 新增 @throws 宣告,並且描述丟擲異常的場景。
/**
* Method description
*
* @throws MyBusinessException - businuess exception description
*/
public void doSomething(String input) throws MyBusinessException {
// ...
}
同時,在丟擲MyBusinessException 異常時,需要儘可能精確地描述問題和相關資訊,這樣無論是列印到紀錄檔中還是在監控工具中,都能夠更容易被人閱讀,從而可以更好地定位具體錯誤資訊、錯誤的嚴重程度等。
大多數 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)
}
}
Throwable 是所有異常和錯誤的超類。你可以在 catch 子句中使用它,但是你永遠不應該這樣做!
如果在 catch 子句中使用 Throwable ,它不僅會捕獲所有異常,也將捕獲所有的錯誤。JVM 丟擲錯誤,指出不應該由應用程式處理的嚴重問題。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。兩者都是由應用程式控制之外的情況引起的,無法處理。
所以,最好不要捕獲 Throwable ,除非你確定自己處於一種特殊的情況下能夠處理錯誤。
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
很多時候,開發者很有自信不會丟擲異常,因此寫了一個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
}
}
這可能是本文中最常被忽略的最佳實踐。 可以發現很多程式碼甚至類庫中都會有捕獲異常、記錄紀錄檔並再次丟擲的邏輯。如下:
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);
}
}
因此,僅僅當想要處理異常時才去捕獲,否則只需要在方法簽名中宣告讓呼叫者去處理。
捕獲標準異常幷包裝為自定義異常是一個很常見的做法。這樣可以新增更為具體的異常資訊並能夠做針對的例外處理。
在你這樣做時,請確保將原始異常設定為原因(注:參考下方程式碼 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);
}
}
不應該使用異常控制應用的執行流程,例如,本應該使用if語句進行條件判斷的情況下,你卻使用例外處理,這是非常不好的習慣,會嚴重影響應用的效能。
try塊中的return語句執行成功後,並不馬上返回,而是繼續執行finally塊中的語句,如果此處存在return語句,則在此直接返回,無情丟棄掉try塊中的返回點。
如下是一個反例:
private int x = 0;
public int checkReturn() {
try {
// x等於1,此處不返回
return ++x;
} finally {
// 返回的結果是2
return ++x;
}
}
這邊詳細介紹了異常的概念、原理,以及在應用中的一些小結。異常的能力是我們快速定位程式錯誤的重要手段之一,也是我們不斷優化程式,提高程式健壯性的依據,所以熟練掌握異常的使用是非常有必要的。