Java 異常處理
Java
的異常處理是 Java 語言的一大重要特性,也是提高代碼健壯性的最強大方法之一。當我們編寫了錯誤的代碼時,編譯器在編譯期間可能會拋出異常,有時候即使編譯正常,在運行代碼的時候也可能會拋出異常。本小節我們將介紹什么是異常、Java 中異常類的架構、如何進行異常處理、如何自定義異常、什么是異常鏈、如何使用異常鏈等內容。
1. 什么是異常
異常就是程序上的錯誤,我們在編寫程序的時候經常會產生錯誤,這些錯誤劃分為編譯期間的錯誤和運行期間的錯誤。
下面我們來看幾個常見的異常案例。
如果語句漏寫分號,程序在編譯期間就會拋出異常,實例如下:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!")
}
}
運行結果:
$ javac Hello.java
Hello.java:3: 錯誤: 需要';'
System.out.println("Hello World!")
^
1 個錯誤
運行過程:
由于代碼的第 3 行語句漏寫了分號,Java 編譯器給出了明確的提示。
static
關鍵字寫成了 statci
,實例如下:
Hello.java:2: 錯誤: 需要<標識符>
public statci void main(String[] args) {
^
1 個錯誤
當數組下標越界,程序在編譯階段不會發生錯誤,但在運行時會拋出異常。實例如下:
public class ArrayOutOfIndex {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
System.out.println(arr[3]);
}
}
運行結果:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at ArrayOutOfIndex.main(ArrayOutOfIndex.java:4)
運行過程:
2. Java 異常類架構
在 Java 中,通過 Throwable
及其子類來描述各種不同類型的異常。如下是 Java 異常類的架構圖(不是全部,只展示部分類):
2.1 Throwable 類
Throwable
位于 java.lang
包下,它是 Java 語言中所有錯誤(Error
)和異常(Exception
)的父類。
Throwable
包含了其線程創建時線程執行堆棧的快照,它提供了 printStackTrace()
等接口用于獲取堆棧跟蹤數據等信息。
主要方法:
-
fillInStackTrace
: 用當前的調用棧層次填充Throwable
對象棧層次,添加到棧層次任何先前信息中; -
getMessage
:返回關于發生的異常的詳細信息。這個消息在Throwable
類的構造函數中初始化了; -
getCause
:返回一個Throwable
對象代表異常原因; -
getStackTrace
:返回一個包含堆棧層次的數組。下標為 0 的元素代表棧頂,最后一個元素代表方法調用堆棧的棧底; -
printStackTrace
:打印toString()
結果和棧層次到System.err
,即錯誤輸出流。
2.2 Error 類
Error
是 Throwable
的一個直接子類,它可以指示合理的應用程序不應該嘗試捕獲的嚴重問題。這些錯誤在應用程序的控制和處理能力之外,編譯器不會檢查 Error
,對于設計合理的應用程序來說,即使發生了錯誤,本質上也無法通過異常處理來解決其所引起的異常狀況。
常見 Error
:
-
AssertionError
:斷言錯誤; -
VirtualMachineError
:虛擬機錯誤; -
UnsupportedClassVersionError
:Java 類版本錯誤; -
OutOfMemoryError
:內存溢出錯誤。
2.3 Exception 類
Exception
是 Throwable
的一個直接子類。它指示合理的應用程序可能希望捕獲的條件。
Exception
又包括 Unchecked Exception
(非檢查異常)和 Checked Exception
(檢查異常)兩大類別。
2.3.1 Unchecked Exception (非檢查異常)
Unchecked Exception
是編譯器不要求強制處理的異常,包含 RuntimeException
以及它的相關子類。我們編寫代碼時即使不去處理此類異常,程序還是會編譯通過。
常見非檢查異常:
-
NullPointerException
:空指針異常; -
ArithmeticException
:算數異常; -
ArrayIndexOutOfBoundsException
:數組下標越界異常; -
ClassCastException
:類型轉換異常。
2.3.2 Checked Exception(檢查異常)
Checked Exception
是編譯器要求必須處理的異常,除了 RuntimeException
以及它的子類,都是 Checked Exception
異常。我們在程序編寫時就必須處理此類異常,否則程序無法編譯通過。
常見檢查異常:
-
IOException
:IO 異常 -
SQLException
:SQL 異常
3. 如何進行異常處理
在 Java 語言中,異常處理機制可以分為兩部分:
-
拋出異常:當一個方法發生錯誤時,會創建一個異常對象,并交給運行時系統處理;
-
捕獲異常:在方法拋出異常之后,運行時系統將轉為尋找合適的異常處理器。
Java 通過 5 個關鍵字來實現異常處理,分別是:throw
、throws
、try
、catch
、finally
。
異常總是先拋出,后捕獲的。下面我們將圍繞著 5 個關鍵字來詳細講解如何拋出異常以及如何捕獲異常。
4. 拋出異常
4.1 實例
我們先來看一個除零異常的實例代碼:
public class ExceptionDemo1 {
// 打印 a / b 的結果
public static void divide(int a, int b) {
System.out.println(a / b);
}
public static void main(String[] args) {
// 調用 divide() 方法
divide(2, 0);
}
}
運行結果:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ExceptionDemo1.divide(ExceptionDemo1.java:4)
at ExceptionDemo1.main(ExceptionDemo1.java:9)
運行過程:
我們知道 0
是不能用作除數的,由于 divide()
方法中除數 b
為 0
,所以代碼將停止執行并顯示了相關的異常信息,此信息為堆棧跟蹤,上面的運行結果告訴我們:main
線程發生了類型為 ArithmeticException
的異常,顯示消息為 by zero
,并且提示了可能發生異常的方法和行號。
4.2 throw
上面的實例中,程序在運行時引發了錯誤,那么如何來顯示拋出(創建)異常呢?
我們可以使用 throw
關鍵字來拋出異常,throw
關鍵字后面跟異常對象,改寫上面的實例代碼:
public class ExceptionDemo2 {
// 打印 a / b 的結果
public static void divide(int a, int b) {
if (b == 0) {
// 拋出異常
throw new ArithmeticException("除數不能為零");
}
System.out.println(a / b);
}
public static void main(String[] args) {
// 調用 divide() 方法
divide(2, 0);
}
}
運行結果:
Exception in thread "main" java.lang.ArithmeticException: 除數不能為零
at ExceptionDemo2.divide(ExceptionDemo2.java:5)
at ExceptionDemo2.main(ExceptionDemo2.java:12)
運行過程:
代碼在運行時同樣引發了錯誤,但顯示消息為 “除數不能為零”。我們看到 divide()
方法中加入了條件判斷,如果調用者將參數 b
設置為 0
時,會使用 throw
關鍵字來拋出異常,throw 后面跟了一個使用 new
關鍵字實例化的算數異常對象,并且將消息字符串作為參數傳遞給了算數異常的構造函數。
我們可以使用 throw
關鍵字拋出任何類型的 Throwable
對象,它會中斷方法,throw
語句之后的所有內容都不會執行。除非已經處理拋出的異常。異常對象不是從方法中返回的,而是從方法中拋出的。
4.3 throws
可以通過 throws
關鍵字聲明方法要拋出何種類型的異常。如果一個方法可能會出現異常,但是沒有能力處理這種異常,可以在方法聲明處使用 throws
關鍵字來聲明要拋出的異常。例如,汽車在運行時可能會出現故障,汽車本身沒辦法處理這個故障,那就讓開車的人來處理。
throws
用在方法定義時聲明該方法要拋出的異常類型,如下是偽代碼:
public void demoMethod() throws Exception1, Exception2, ... ExceptionN {
// 可能產生異常的代碼
}
throws
后面跟的異常類型列表可以有一個也可以有多個,多個則以 ,
分割。當方法產生異常列表中的異常時,將把異常拋向方法的調用方,由調用方處理。
throws 有如下使用規則:
- 如果方法中全部是非檢查異常(即
Error
、RuntimeException
以及的子類),那么可以不使用throws
關鍵字來聲明要拋出的異常,編譯器能夠通過編譯,但在運行時會被系統拋出; - 如果方法中可能出現檢查異常,就必須使用
throws
聲明將其拋出或使用try catch
捕獲異常,否則將導致編譯錯誤; - 當一個方法拋出了異常,那么該方法的調用者必須處理或者重新拋出該異常;
- 當子類重寫父類拋出異常的方法時,聲明的異常必須是父類所聲明異常的同類或子類。
5. 捕獲異常
使用 try 和 catch 關鍵字可以捕獲異常。try catch 代碼塊放在異常可能發生的地方。它的語法如下:
try {
// 可能會發生異常的代碼塊
} catch (Exception e1) {
// 捕獲并處理try拋出的異常類型Exception
} catch (Exception2 e2) {
// 捕獲并處理try拋出的異常類型Exception2
} finally {
// 無論是否發生異常,都將執行的代碼塊
}
我們來看一下上面語法中的 3 種語句塊:
try
語句塊:用于監聽異常,當發生異常時,異常就會被拋出;catch
語句塊:catch
語句包含要捕獲的異常類型的聲明,當try
語句塊發生異常時,catch
語句塊就會被檢查。當catch
塊嘗試捕獲異常時,是按照catch
塊的聲明順序從上往下尋找的,一旦匹配,就不會再向下執行。因此,如果同一個try
塊下的多個catch
異常類型有父子關系,應該將子類異常放在前面,父類異常放在后面;finally
語句塊:無論是否發生異常,都會執行finally
語句塊。finally
常用于這樣的場景:由于finally
語句塊總是會被執行,所以那些在try
代碼塊中打開的,并且必須回收的物理資源(如數據庫連接、網絡連接和文件),一般會放在finally
語句塊中釋放資源。
try
語句塊后可以接零個或多個 catch
語句塊,如果沒有 catch
塊,則必須跟一個 finally
語句塊。簡單來說,try
不允許單獨使用,必須和 catch
或 finally
組合使用,catch
和 finally
也不能單獨使用。
實例如下:
public class ExceptionDemo3 {
// 打印 a / b 的結果
public static void divide(int a, int b) {
System.out.println(a / b);
}
public static void main(String[] args) {
try {
// try 語句塊
// 調用 divide() 方法
divide(2, 0);
} catch (ArithmeticException e) {
// catch 語句塊
System.out.println("catch: 發生了算數異常:" + e);
} finally {
// finally 語句塊
System.out.println("finally: 無論是否發生異常,都會執行");
}
}
}
運行結果:
catch: 發生了算數異常:java.lang.ArithmeticException: / by zero
finally: 無論是否發生異常,都會執行
運行過程:

divide()
方法中除數 b
為 0
,會發生除零異常,我們在方法調用處使用了 try
語句塊對異常進行捕獲;如果捕獲到了異常, catch
語句塊會對 ArithmeticException
類型的異常進行處理,此處打印了一行自定義的提示語句;最后的 finally
語句塊,無論發生異常與否,總會執行。
Java 7 以后,catch
多種異常時,也可以像下面這樣簡化代碼:
try {
// 可能會發生異常的代碼塊
} catch (Exception | Exception2 e) {
// 捕獲并處理try拋出的異常類型
} finally {
// 無論是否發生異常,都將執行的代碼塊
}
6. 自定義異常
自定義異常,就是定義一個類,去繼承 Throwable
類或者它的子類。
Java 內置了豐富的異常類,通常使用這些內置異常類,就可以描述我們在編碼時出現的大部分異常情況。一旦內置異常無法滿足我們的業務要求,就可以通過自定義異常描述特定業務產生的異常類型。
實例:
public class ExceptionDemo4 {
static class MyCustomException extends RuntimeException {
/**
* 無參構造方法
*/
public MyCustomException() {
super("我的自定義異常");
}
}
public static void main(String[] args) {
// 直接拋出異常
throw new MyCustomException();
}
}
運行結果:
Exception in thread "main" ExceptionDemo4$MyCustomException: 我的自定義異常
at ExceptionDemo4.main(ExceptionDemo4.java:13)
運行過程:

在代碼中寫了一個自定義異常 MyCustomException
,繼承自 RuntimeException
,它是一個靜態內部類,這樣在主方法中就可以直接拋出這個異常類了。當然,也可以使用 catch
來捕獲此類型異常。
7. 異常鏈
異常鏈是以一個異常對象為參數構造新的異常對象,新的異常對象將包含先前異常的信息。簡單來說,就是將異常信息從底層傳遞給上層,逐層拋出,我們來看一個實例:
public class ExceptionDemo5 {
/**
* 第一個自定義的靜態內部異常類
*/
static class FirstCustomException extends Exception {
// 無參構造方法
public FirstCustomException() {
super("第一個異常");
}
}
/**
* 第二個自定義的靜態內部異常類
*/
static class SecondCustomException extends Exception {
public SecondCustomException() {
super("第二個異常");
}
}
/**
* 第三個自定義的靜態內部異常類
*/
static class ThirdCustomException extends Exception {
public ThirdCustomException() {
super("第三個異常");
}
}
/**
* 測試異常鏈靜態方法1,直接拋出第一個自定義的靜態內部異常類
* @throws FirstCustomException
*/
public static void f1() throws FirstCustomException {
throw new FirstCustomException();
}
/**
* 測試異常鏈靜態方法2,調用f1()方法,并拋出第二個自定義的靜態內部異常類
* @throws SecondCustomException
*/
public static void f2() throws SecondCustomException {
try {
f1();
} catch (FirstCustomException e) {
throw new SecondCustomException();
}
}
/**
* 測試異常鏈靜態方法3,調用f2()方法, 并拋出第三個自定義的靜態內部異常類
* @throws ThirdCustomException
*/
public static void f3() throws ThirdCustomException {
try {
f2();
} catch (SecondCustomException e) {
throw new ThirdCustomException();
}
}
public static void main(String[] args) throws ThirdCustomException {
// 調用靜態方法f3()
f3();
}
}
運行結果:
Exception in thread "main" ExceptionDemo5$ThirdCustomException: 第三個異常
at ExceptionDemo5.f3(ExceptionDemo5.java:46)
at ExceptionDemo5.main(ExceptionDemo5.java:51)
運行過程:

通過運行結果,我們只獲取到了靜態方法 f3()
所拋出的異常堆棧信息,前面代碼所拋出的異常并沒有被顯示。
我們改寫上面的代碼,讓異常信息以鏈條的方式 “連接” 起來。可以通過改寫自定義異常的構造方法,來獲取到之前異常的信息。實例如下:
/**
* @author colorful@TaleLin
*/
public class ExceptionDemo6 {
/**
* 第一個自定義的靜態內部異常類
*/
static class FirstCustomException extends Exception {
// 無參構造方法
public FirstCustomException() {
super("第一個異常");
}
}
/**
* 第二個自定義的靜態內部異常類
*/
static class SecondCustomException extends Exception {
/**
* 通過構造方法獲取之前異常的信息
* @param cause 捕獲到的異常對象
*/
public SecondCustomException(Throwable cause) {
super("第二個異常", cause);
}
}
/**
* 第三個自定義的靜態內部異常類
*/
static class ThirdCustomException extends Exception {
/**
* 通過構造方法獲取之前異常的信息
* @param cause 捕獲到的異常對象
*/
public ThirdCustomException(Throwable cause) {
super("第三個異常", cause);
}
}
/**
* 測試異常鏈靜態方法1,直接拋出第一個自定義的靜態內部異常類
* @throws FirstCustomException
*/
public static void f1() throws FirstCustomException {
throw new FirstCustomException();
}
/**
* 測試異常鏈靜態方法2,調用f1()方法,并拋出第二個自定義的靜態內部異常類
* @throws SecondCustomException
*/
public static void f2() throws SecondCustomException {
try {
f1();
} catch (FirstCustomException e) {
throw new SecondCustomException(e);
}
}
/**
* 測試異常鏈靜態方法3,調用f2()方法, 并拋出第三個自定義的靜態內部異常類
* @throws ThirdCustomException
*/
public static void f3() throws ThirdCustomException {
try {
f2();
} catch (SecondCustomException e) {
throw new ThirdCustomException(e);
}
}
public static void main(String[] args) throws ThirdCustomException {
// 調用靜態方法f3()
f3();
}
}
運行結果:
Exception in thread "main" ExceptionDemo6$ThirdCustomException: 第三個異常
at ExceptionDemo6.f3(ExceptionDemo6.java:74)
at ExceptionDemo6.main(ExceptionDemo6.java:80)
Caused by: ExceptionDemo6$SecondCustomException: 第二個異常
at ExceptionDemo6.f2(ExceptionDemo6.java:62)
at ExceptionDemo6.f3(ExceptionDemo6.java:72)
... 1 more
Caused by: ExceptionDemo6$FirstCustomException: 第一個異常
at ExceptionDemo6.f1(ExceptionDemo6.java:51)
at ExceptionDemo6.f2(ExceptionDemo6.java:60)
... 2 more
運行過程:

通過運行結果,我們看到,異常發生的整個過程都打印到了屏幕上,這就是一個異常鏈。
8. 小結
通過本小節的學習,我們知道了異常就是程序上的錯誤,良好的異常處理可以提高代碼的健壯性。Java 語言中所有錯誤(Error
)和異常(Exception
)的父類都是 Throwable
。Error
和 Exception
是 Throwable
的直接子類,我們通常說的異常處理實際上就是處理 Exception
及其子類,異常又分為檢查型異常和非檢查型異常。通過拋出異常和捕獲異常來實現異常處理。我們亦可以通過繼承 Throwable
類或者它的子類來自定義異常類。通過構造方法獲取之前異常的信息可以實現異常鏈。