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

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

在代碼中寫了一個(gè)自定義異常 MyCustomException
,繼承自 RuntimeException
,它是一個(gè)靜態(tài)內(nèi)部類,這樣在主方法中就可以直接拋出這個(gè)異常類了。當(dāng)然,也可以使用 catch
來(lái)捕獲此類型異常。
7. 異常鏈
異常鏈?zhǔn)且砸粋€(gè)異常對(duì)象為參數(shù)構(gòu)造新的異常對(duì)象,新的異常對(duì)象將包含先前異常的信息。簡(jiǎn)單來(lái)說(shuō),就是將異常信息從底層傳遞給上層,逐層拋出,我們來(lái)看一個(gè)實(shí)例:
public class ExceptionDemo5 {
/**
* 第一個(gè)自定義的靜態(tài)內(nèi)部異常類
*/
static class FirstCustomException extends Exception {
// 無(wú)參構(gòu)造方法
public FirstCustomException() {
super("第一個(gè)異常");
}
}
/**
* 第二個(gè)自定義的靜態(tài)內(nèi)部異常類
*/
static class SecondCustomException extends Exception {
public SecondCustomException() {
super("第二個(gè)異常");
}
}
/**
* 第三個(gè)自定義的靜態(tài)內(nèi)部異常類
*/
static class ThirdCustomException extends Exception {
public ThirdCustomException() {
super("第三個(gè)異常");
}
}
/**
* 測(cè)試異常鏈靜態(tài)方法1,直接拋出第一個(gè)自定義的靜態(tài)內(nèi)部異常類
* @throws FirstCustomException
*/
public static void f1() throws FirstCustomException {
throw new FirstCustomException();
}
/**
* 測(cè)試異常鏈靜態(tài)方法2,調(diào)用f1()方法,并拋出第二個(gè)自定義的靜態(tài)內(nèi)部異常類
* @throws SecondCustomException
*/
public static void f2() throws SecondCustomException {
try {
f1();
} catch (FirstCustomException e) {
throw new SecondCustomException();
}
}
/**
* 測(cè)試異常鏈靜態(tài)方法3,調(diào)用f2()方法, 并拋出第三個(gè)自定義的靜態(tài)內(nèi)部異常類
* @throws ThirdCustomException
*/
public static void f3() throws ThirdCustomException {
try {
f2();
} catch (SecondCustomException e) {
throw new ThirdCustomException();
}
}
public static void main(String[] args) throws ThirdCustomException {
// 調(diào)用靜態(tài)方法f3()
f3();
}
}
運(yùn)行結(jié)果:
Exception in thread "main" ExceptionDemo5$ThirdCustomException: 第三個(gè)異常
at ExceptionDemo5.f3(ExceptionDemo5.java:46)
at ExceptionDemo5.main(ExceptionDemo5.java:51)
運(yùn)行過(guò)程:

通過(guò)運(yùn)行結(jié)果,我們只獲取到了靜態(tài)方法 f3()
所拋出的異常堆棧信息,前面代碼所拋出的異常并沒(méi)有被顯示。
我們改寫上面的代碼,讓異常信息以鏈條的方式 “連接” 起來(lái)。可以通過(guò)改寫自定義異常的構(gòu)造方法,來(lái)獲取到之前異常的信息。實(shí)例如下:
/**
* @author colorful@TaleLin
*/
public class ExceptionDemo6 {
/**
* 第一個(gè)自定義的靜態(tài)內(nèi)部異常類
*/
static class FirstCustomException extends Exception {
// 無(wú)參構(gòu)造方法
public FirstCustomException() {
super("第一個(gè)異常");
}
}
/**
* 第二個(gè)自定義的靜態(tài)內(nèi)部異常類
*/
static class SecondCustomException extends Exception {
/**
* 通過(guò)構(gòu)造方法獲取之前異常的信息
* @param cause 捕獲到的異常對(duì)象
*/
public SecondCustomException(Throwable cause) {
super("第二個(gè)異常", cause);
}
}
/**
* 第三個(gè)自定義的靜態(tài)內(nèi)部異常類
*/
static class ThirdCustomException extends Exception {
/**
* 通過(guò)構(gòu)造方法獲取之前異常的信息
* @param cause 捕獲到的異常對(duì)象
*/
public ThirdCustomException(Throwable cause) {
super("第三個(gè)異常", cause);
}
}
/**
* 測(cè)試異常鏈靜態(tài)方法1,直接拋出第一個(gè)自定義的靜態(tài)內(nèi)部異常類
* @throws FirstCustomException
*/
public static void f1() throws FirstCustomException {
throw new FirstCustomException();
}
/**
* 測(cè)試異常鏈靜態(tài)方法2,調(diào)用f1()方法,并拋出第二個(gè)自定義的靜態(tài)內(nèi)部異常類
* @throws SecondCustomException
*/
public static void f2() throws SecondCustomException {
try {
f1();
} catch (FirstCustomException e) {
throw new SecondCustomException(e);
}
}
/**
* 測(cè)試異常鏈靜態(tài)方法3,調(diào)用f2()方法, 并拋出第三個(gè)自定義的靜態(tài)內(nèi)部異常類
* @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 {
// 調(diào)用靜態(tài)方法f3()
f3();
}
}
運(yùn)行結(jié)果:
Exception in thread "main" ExceptionDemo6$ThirdCustomException: 第三個(gè)異常
at ExceptionDemo6.f3(ExceptionDemo6.java:74)
at ExceptionDemo6.main(ExceptionDemo6.java:80)
Caused by: ExceptionDemo6$SecondCustomException: 第二個(gè)異常
at ExceptionDemo6.f2(ExceptionDemo6.java:62)
at ExceptionDemo6.f3(ExceptionDemo6.java:72)
... 1 more
Caused by: ExceptionDemo6$FirstCustomException: 第一個(gè)異常
at ExceptionDemo6.f1(ExceptionDemo6.java:51)
at ExceptionDemo6.f2(ExceptionDemo6.java:60)
... 2 more
運(yùn)行過(guò)程:

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