《Java程式設計思想》裡面有一句話:Java的基本理念是“結構不佳的程式碼是不能執行的”。個人覺得,這可以從兩個層面來理解,一是程式碼自身問題,有錯誤(在編譯時期或者執行時期出現錯誤)的程式碼是不能繼續執行下去的。二是開發者對程式碼質量的極致要求,我們決不允許有影響系統的正常執行的程式碼存在。
對於編譯時期出現錯誤的程式碼我們可以及時發現修正,但是對於執行期出現異常的程式碼我們是很難提前發現並修正的。這就需要我們學會捕獲異常並處理異常,保證系統的穩定執行。
什麼是異常
在《Java程式設計思想》裡面就說的很清楚了:
異常情形是指阻止當前方法或者作用域繼續執行的問題。把異常問題和普通問題相區分很重要,所謂普通問題是指,在當前環境下能夠得到足夠的資訊,總能處理這個問題。而對於異常情形,就不能繼續下去了,因為在當前環境下無法獲得必要的資訊來解決問題。你所能做的就是從當前的環境跳出,並把問題交給上一級。這就是丟擲異常所發生的事情。
簡單來說異常就是阻止程式繼續執行下去,這時候JVM就會丟擲異常。
在JVM丟擲異常時,會在堆上建立異常物件,然後當前的執行路徑被終止,並且從當前環境中彈出剛才建立的異常物件的引用。接著,異常處理機制接管程式。如果有catch、finally語句就會在catch語句處理異常並執行finally語句。當然,可以沒有finally語句,但是如果需要處理異常需要在catch語句裡面執行。雖然finlly語句最終也會被執行,但是建議在catch語句處理異常,同時catch接受異常物件作為引數,方便異常的處理。
異常的分類
Java中的異常大致可以分為兩類:編譯時期異常和執行時異常。編譯時期異常就是在程式碼編譯階段可以發現的異常,比如使用javac
命令編譯程式碼,如果程式碼有錯誤就會編譯不過並提示錯誤原因。執行時異常是在程式碼執行的時候我們才會知道的異常,比如除數為0,陣列溢位等等異常。
對於開發者來說,如果使用IDE開發程式,編譯時期異常可以交由IDE找出,對於執行時異常我們只能是在程式碼中進行處理。處理方法有兩樣:丟擲異常和捕獲異常。
關於異常的丟擲和捕獲
開發者可能有疑問,對於異常,我們是丟擲好呢還是捕獲好呢?兩者沒有孰優孰劣,大致處理思路是這樣:如果我們知道如何處理異常就儘量不要丟擲,如果不知道程式碼呼叫者可能會發生的情形就儘量丟擲異常給呼叫者處理。誠然,有時候呼叫者也不知道如何處理異常,這時候個人覺得還是丟擲異常給呼叫者好,因為這樣有利於日後程式碼的維護和擴充。
為什要對異常進行處理
最容易想到的是保證系統的穩定性。這是其中原因之一。對異常處理,並適當輸出異常日誌,這樣有助於我們日後對系統定位問題。還有一點個人感受特別深就是:不揹他人的鍋。我們在開發系統的時候,呼叫第三方服務的情景是非常常見的。有一次參與的專案的一個介面在前端呼叫失敗了,翻看錯誤日誌,發現是在呼叫第三方服務的時候出現網路超時。但是,第三方服務提供方除錯那邊的服務沒有問題。如果這時候沒有對呼叫第三方服務進行異常處理並輸出日誌,這鍋只能自己背唄。同時也不利於錯誤的除錯。最後確定雙方的程式碼都沒有問題,我們很容易就想到可能是伺服器網路出入口的問題。找到運維人員溝通核查問題後,問題馬上就定位了:確實是網路出入口問題。
對異常的正確處理於人於系統都是有很大的好處的。
Java中異常簡單圖譜
Java將可丟擲異常分為三類:受檢查異常、執行時異常(RuntimeException)、錯誤(Error)。所有這些異常都是Throwable的子類
- 受檢查異常:除了RuntimeException和其子類,所有的Exception都是受檢查異常(編譯器會檢查的異常)
- 執行時異常:RuntimeException以及子類
- 錯誤:一般是系統錯誤,不應該捕獲的異常
如何捕獲異常
對於可能丟擲異常的程式碼或者已經丟擲異常的程式碼我們應該怎麼捕獲和處理呢?Java給我們提供了try...catch...finally語句:語法:
try {
// 捕獲異常
} catch(Exception e) {
// 處理異常
} finally {
// 無論try語句是否捕獲了異常,finally語句的程式碼都會執行
}
複製程式碼
下面我們看兩段程式碼:
程式碼一:
@Test
public void testDemo() {
String str = exception();
System.out.println("exception方法執行結果:" + str);
}
private String exception() {
int a = 0;
int b = 1;
try {
int c = b / a;
System.out.println("運算結束");
return "b/c = " + c;
} catch (ArithmeticException e) {
System.out.println("catch error");
return " error -> 發生錯誤了";
} finally {
System.out.println("我是finally語句塊內容");
return " finally block code";
}
}
複製程式碼
程式碼二:
@Test
public void testDemo() {
String str = exception();
System.out.println("exception方法執行結果:" + str);
}
private String exception() {
int a = 0;
int b = 1;
try {
int c = b / a;
System.out.println("運算結束");
return "b/c = " + c;
} catch (ArithmeticException e) {
System.out.println("catch error");
return " error -> 發生錯誤了";
} finally {
System.out.println("我是finally語句塊內容");
// return " finally block code";
}
}
複製程式碼
大家想一下,這兩段程式碼分別輸出是什麼,不懂多想幾分鐘再看答案哦。
程式碼一輸出:
catch error
我是finally語句塊內容
exception方法執行結果: finally block code
複製程式碼
程式碼二輸出:
catch error
我是finally語句塊內容
exception方法執行結果: error -> 發生錯誤了
複製程式碼
分析輸出,知道不管在catch語句有沒有返回語句,都會執行finally程式碼塊的程式碼,執行順序是catch->finally,如果finally有返回,就直接返回,沒有就使用catch的返回。大家可以打個斷點除錯就知道執行順序了。
如果catch和finally都沒有返回,這時候程式碼就會報缺少返回值錯誤,需要在方法加上返回值。
private String exception() {
int a = 0;
int b = 1;
try {
int c = b / a;
System.out.println("運算結束");
return "b/c = " + c;
} catch (ArithmeticException e) {
System.out.println("catch error");
// return " error -> 發生錯誤了";
} finally {
System.out.println("我是finally語句塊內容");
// return " finally block code";
}
return "none";
}
複製程式碼
輸出:
catch error
我是finally語句塊內容
exception方法執行結果:none
複製程式碼
當然,沒有catch程式,程式碼還是可以繼續執行:
private String exception() {
int a = 0;
int b = 1;
try {
int c = b / a;
System.out.println("運算結束");
return "b/c = " + c;
}
// catch (ArithmeticException e) {
// System.out.println("catch error");
//// return " error -> 發生錯誤了";
// }
finally {
System.out.println("我是finally語句塊內容");
return " finally block code";
}
// return "none";
}
複製程式碼
輸出:
我是finally語句塊內容
exception方法執行結果: finally block code
複製程式碼
關於多個異常的捕獲
對於多個異常的捕獲,在try後面接上多個catch語句就可以。記得,範圍是從小到大,不然會編譯出錯。在異常丟擲時,會從第一個catch語句開始匹配異常,第一個匹配到的就執行異常處理語句,後面的處理語句就不再執行了。如下程式碼:
@Test
public void testDemo() {
String str = exception();
System.out.println("exception方法執行結果:" + str);
}
private String exception() {
try {
int[] arr = new int[]{1, 2, 3};
System.out.println("arr[3]=" + arr[3]);
return "ok";
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("catch ArrayIndexOutOfBoundsException");
return "catch ArrayIndexOutOfBoundsException";
} catch (Exception e) {
System.out.println("catch Exception");
return "catch ArithmeticException";
} finally {
System.out.println("我是finally語句塊內容");
return "finally";
}
}
複製程式碼
輸出:
catch ArrayIndexOutOfBoundsException
我是finally語句塊內容
exception方法執行結果:finally
複製程式碼
關於手動丟擲異常
上面講的異常都是JVM丟擲的,實際上我們還可以手動丟擲異常。
- 使用throw在語句中丟擲異常
throw new RuntimeException("手動丟擲RuntimeException");
複製程式碼
- 使用throws在方法後面丟擲異常
private String exception() throws RuntimeException {
// 其它的程式碼
}
複製程式碼
關於自定義異常
有時API提供的異常類不足以滿足業務需求,我們可以自定義異常。自定義異常很簡單,建立一個類繼承異常類或者實現異常介面就可以了。這裡就不在詳細介紹。丟擲自定義異常和丟擲API提供的異常沒有什麼兩樣。
自定義異常類,實現我們需要的構造方法就可以了。
public class CustomeException extends RuntimeException {
public CustomeException() {
}
public CustomeException(String message) {
super(message);
}
}
複製程式碼
題外話
關於異常的基礎入門知識已經講完,如果大家還有什麼不懂的可以家QQ群進行討論:599791743。歡迎大家討論指出不足之處。
同時也歡迎大家關注微信公眾號:深夜程猿,閱讀更多文章。