一、前言
??只要寫過Java代碼烤镐,基本上都會遇到異常蛋济,由于以前學(xué)習(xí)的不夠系統(tǒng),所以趁現(xiàn)在有時間炮叶,再來重新回顧及梳理下Java的異常處理碗旅。
二、異常處理
1. 概念
當(dāng)一個用戶在使用我們的程序期間镜悉,如果由于程序的錯誤或一些外部環(huán)境的影響造成用戶數(shù)據(jù)的丟失祟辟,用戶可能就不會再使用這個程序了,為了避免這種事情的發(fā)生侣肄,一般我們的程序應(yīng)該能做到如下幾點:
- 向用戶通報錯誤旧困;
- 保存所有的工作結(jié)果;
- 允許用戶以妥善的形式退出程序稼锅;
針對這種異常情況吼具,Java中使用一種稱為異常處理的錯誤捕獲機(jī)制處理。所謂異常處理矩距,就是將控制權(quán)從錯誤產(chǎn)生的地方轉(zhuǎn)移給能夠處理這種情況的異常處理器(Exception Handler)拗盒,其實處理器的目的就是讓我們以一種友好的交互方式將異常展示給用戶。
2. 異常分類
在Java語言中剩晴,所有的異常都是繼承自Throwable這個基礎(chǔ)類锣咒,我們可以看一個簡單示意圖:
可以看到侵状,在Throwable的直屬子類中又分為了兩部分:Error和Exception。
??Error 表示Java運行時系統(tǒng)的內(nèi)部錯誤和資源耗盡錯誤毅整,應(yīng)用程序不應(yīng)該拋出這種類型的錯誤趣兄,如果真的出現(xiàn)了這樣的內(nèi)部錯誤,處理通告給用戶悼嫉,并盡力使程序安全的終止之外艇潭,我們就再也無能為力了,不過這種情況極少出現(xiàn)戏蔑;
而我們所關(guān)注的重點蹋凝,也就是Exception結(jié)構(gòu),而Exception 的實現(xiàn)類又分為兩部分:繼承自RuntimeException的異常和其他異常总棵。而劃分這兩種分類的規(guī)則是:
由程序錯誤導(dǎo)致的異常屬于RuntimeException鳍寂,而程序本身沒有問題,但由于像I/O 錯誤這類問題導(dǎo)致的異常屬于其他異常情龄。
繼承自RuntimeException的異常迄汛,通常包含以下幾種情況:
- 錯誤的類型轉(zhuǎn)換;
- 數(shù)組訪問越界骤视;
- 訪問的時候產(chǎn)生空指針等鞍爱;
而不是繼承自RuntimeException的異常則包括:
- 試圖在文件尾部后面讀取數(shù)據(jù);
- 打開一個不存在的文件专酗;
- 根據(jù)類的名稱查找類睹逃,但該類不存在等情況;
所以說祷肯,有這么一句話還是很有道理的:
如果出現(xiàn)了RuntimeException異常沉填,那么就一定是你的問題。
3 已檢查異常和未檢查異常
??Java語言規(guī)范將繼承自Error類或者RuntimeException類的所有異常稱為未檢查異常(unchecked)佑笋,而所有其他的異常則稱為已檢查異常(checked)拜轨,而編譯器則會檢查是否為所有的已檢查異常提供了異常處理器。我們先來看下已檢查異常允青。
3.1 已檢查異常
??已檢查異常也就是說橄碾,一個方法不僅需要告訴編譯器將要返回什么值,還要告訴編譯器有可能發(fā)生什么錯誤颠锉,比如法牲,一段讀取問文件的代碼 知道有可能讀取的文件不存在,因此在讀取的時候就需要拋出FileNotFoundException琼掠。方法應(yīng)該在其首部聲明所有可能拋出的異常拒垃,這樣就可以從首部反映出這個方法可能拋出哪類已檢查異常。
比如FileInputStream的構(gòu)造方法:
public FileInputStream(String name) throws FileNotFoundException
針對我們定義的有可能被他人調(diào)用的方法瓷蛙,我們應(yīng)該根據(jù)異常規(guī)范悼瓮,在方法的首部聲明這個方法可能拋出的已檢查異常戈毒,如果有多個的話,需要使用逗號分開:
public static void callable() throws ExecutionException, InterruptedException, TimeoutException
而通常情況下横堡,有兩種情況需要我們拋出異常:
- 調(diào)用一個拋出已檢查異常的方法埋市,例如FileInputStream構(gòu)造器;
- 程序運行中出現(xiàn)錯誤命贴,并且利用throw 語句拋出一個已檢查異常;
3.2 未檢查異常
??對未檢查異常胸蛛,我們不用像已檢查異常那樣在方法的首部聲明可能拋出的異常污茵,因為運行時異常完全是可控的,就是說,我們應(yīng)該通過程序盡量避免未檢查異常的發(fā)生零蓉。而如果是Error異常的話,自然不用手動拋出津肛,任何代碼都有拋出Error異常的風(fēng)險身坐,而我們是無法控制該類異常的部蛇。
所以說,一個方法需要聲明所有可能拋出的已檢查異常抹腿,而未檢查異常要么不可控制(Error)警绩,要么就應(yīng)該避免發(fā)生(RuntimeException)后室。
可能需要注意一點,就是子類繼承父類方法問題:
子類方法中聲明的異常不能比父類的范圍更大檀蹋,也就是說,子類可以拋出更特定的異常或者不拋出異常;而如果父類中沒有拋出任何異常,那么子類也不能拋出任何已檢查異常拳魁。
當(dāng)然,除了拋出異常,還可以捕獲異常,在下文我們會來介紹異常的捕獲。
3. 拋出異常
??我們是通過 throw
關(guān)鍵字來拋出異常的,比如說正驻,讀取文件的時候迈倍,如果文件內(nèi)容不包含我們所需要的迹鹅,那我們可以手動拋出一個IOException;再比如弟蚀,如果某一個對象不能為空捶闸,那么我們可以拋出一個繼承自RuntimeException的自定義異常:
public static void main(String[] args) throws IOException {
...
String content = "";
if (!content.contains("hello")) {
throw new IOException();
}
}
public static void main(String[] args) {
String content = null;
if (content == null) {
throw new RuntimeException();
}
}
相應(yīng)的发绢,如果是拋出已檢查異常边酒,需要在方法首部進(jìn)行聲明該異常。
4. 自定義異常
??如果已有的異常滿足不了我們的需求的話牛哺,我們可以選擇自定義異常,一般情況下可以選擇繼承自Exception或者Exception子類的類。而習(xí)慣上蠢古,該類應(yīng)該至少包含兩個構(gòu)造器脊框,一個是默認(rèn)的構(gòu)造器昭灵,另一個是帶有詳細(xì)描述信息的構(gòu)造器:
public class OrderException extends Exception {
public OrderException() {
}
public OrderException(String message) {
super(message);
}
}
同樣,我們可以選擇自定義的異常時check exception或者unchek exception柄冲。
5. 捕獲異常
5.1 捕獲單個異常
??前面我們了解了拋出異常脯颜,拋出異常其實很簡單剔宪,但有些情況下我們需要把異常給捕獲帮毁,比如說直接展示給用戶的時候阶剑,我們需要把異常以一種更直觀更優(yōu)雅的方式展示給用戶外莲。這時候我們就可以通過 try catch finally語句塊來處理。
- 對于資源的關(guān)閉媳瞪,在JDK1.7 之前,我們一般都是通過在finally塊中手動關(guān)閉绞铃,而JDK7引入了
try-with-resources
代碼塊,也就是說 try可以添加資源,這樣該資源會在try語句介紹的時候自動關(guān)閉(多個按順序)鞭缭,不用再手動在finally塊中進(jìn)行關(guān)閉;而如果是多個資源的話芜繁,同樣和普通的代碼一樣塔淤,使用分號進(jìn)行分割喉镰;但是需要注意的是,在try里面的資源惭笑,需要實現(xiàn)了java.lang.AutoCloseable接口侣姆;
static String readFirstLineFromFileWithFinallyBlock(String path)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
針對異常的捕獲,通常沉噩,最好的選擇是什么也不做捺宗,而是將異常傳遞給調(diào)用者:
如果read方法出現(xiàn)了異常,就讓read方法的調(diào)用者去操心川蒙!但如果采用這種處理方法蚜厉,就必須聲明這個方法可能會拋出的異常。
針對捕獲異常畜眨,同樣需要注意的是繼承問題昼牛,比如說,如果父類的方法沒有拋出異常康聂,那么子類的方法就必須捕獲方法中出現(xiàn)的每一個已檢查異常匾嘱,不允許在子類的throws 中出現(xiàn)超過父類方法所列出的異常類范圍。
至于為什么try里面的資源要實現(xiàn)java.lang.AutoCloseable接口早抠,因為該接口提供了一個close方法霎烙,try塊正常退出或者發(fā)生異常退出時,都會自動調(diào)用res.close()蕊连,相當(dāng)于使用了finally塊:
void close() throws Exception;
不過可能還需要注意另一個接口:Closeable
悬垃,該接口實現(xiàn)自AutoCloseable
,也包含一個close方法甘苍,不過尝蠕,這個方法聲明拋出的異常是IOException。
5.2 捕獲多個異常
在一個try語句塊中可以使用多個catch捕獲多個異常载庭,但JDK7之后看彼,同一個catch子句中可以捕獲多個異常類型:
// before JDK 7
try {
...
} catch (FileNotFoundException ex) {
...
} catch (Exception e) {
...
}
// after JDK 7
try {
...
} catch (FileNotFoundException | UnknownHostException e) {
...
} catch (Exception ex) {
...
}
- 針對第一種多個catch的情況廊佩,捕獲的時候注意異常的包含關(guān)系,異常范圍越大的越往后靖榕,比如Exception應(yīng)該放到最后标锄;
- 第二種情況,使用 | 捕獲多個異常的時候茁计,捕獲的異常類型彼此之間不能存在繼承關(guān)系料皇,并且這種情況下,異常變量e隱含為final類型星压,所以不能對該類型執(zhí)行賦值等操作践剂;
6. 再次拋出異常于異常鏈
??在catch子句中可以拋出一個異常,這樣做的目的是改變異常的類型娜膘,比如說方法中發(fā)生了一個已檢查異常但該方法不允許拋出異常逊脯,所以我們可以捕獲后將其包裝成一個運行時異常再拋出,如:
try {
} catch (SQLException e) {
throw new MyException("database error:" + e.getMessage());
}
但還有一種有好的處理方法竣贪,并且將原始異常信息設(shè)置為新異常的信息:
try {
} catch (SQLException e) {
Throwable se = new MyException("database error");
se.initCause(e);
throw se;
}
這樣當(dāng)捕獲到異常時男窟,就可以使用下面這條語句重新得到原始異常:
Throwable e = se.getCause();
官方建議使用這種包裝技術(shù),這樣可以讓用戶拋出子系統(tǒng)中的高級異常贾富,而不會丟失原始異常的細(xì)節(jié)歉眷。
有一點可能需要注意,就是下面這種:
public static void main(String[] args) throws FileNotFoundException {
try {
InputStream inputStream = new FileInputStream("");
...
} catch (Exception e) {
// 日志記錄
throw e;
}
}
首先颤枪,try塊里面有已檢查異常FileNotFoundException汗捡,這時候我們捕獲Exception異常之后,只想做個記錄畏纲,然后再直接拋出扇住,如果try塊里只有一個已檢查異常,并且在catch中該異常未發(fā)生任何變化盗胀,這時候我們在方法首部聲明的要拋出的異乘姨#可以是try塊里唯一的一個已檢查類型FileNotFoundException,因為編譯器會跟蹤try塊里的異常(注意JDK7之后)票灰。
7. finally塊
??finally塊通常用來執(zhí)行一些資源的關(guān)閉女阀,鎖的釋放等操作,因為無論是否發(fā)生異常屑迂,finall塊中的代碼都會被執(zhí)行浸策。這里官方建議使用嵌套的try/catch 和try/finally語句塊,比如:
InputStream inputStream = null;
try {
try {
new FileInputStream("");
// do something
} finally {
inputStream.close();
}
} catch (Exception e) {
// 日志記錄
}
??內(nèi)層的try/finally 只有一個職責(zé)惹盼,就是確保關(guān)閉輸入流庸汗。外出的try/catch就是捕獲出現(xiàn)的異常,這種設(shè)計方式不僅清楚手报,而且還有一個功能就是會捕獲finally 子句中出現(xiàn)的異常蚯舱。
而當(dāng)try/catch/finally塊包含return語句時改化,是一件比較有意思的事,我們放到最后借助例子來了解枉昏。
8. 堆棧跟蹤
??堆棧跟蹤(stack trace)是一個方法調(diào)用過程的列表陈肛,它包含了程序執(zhí)行過程中方法調(diào)用的特定位置,當(dāng)Java程序正常終止凶掰,而沒有捕獲異常時,這個列表就會展示出來蜈亩,比如:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at jdk8.thread.CallableTest.test(CallableTest.java:21)
at jdk8.thread.CallableTest.main(CallableTest.java:17)
我們可以調(diào)用Throwable類的printStackTrace
方法訪問堆棧跟蹤的文本描述信息懦窘,另一種更靈活的辦法是使用getStackTrace
方法,它會得到一個StackTraceElement對象數(shù)組:
Throwable throwable = new Throwable();
StackTraceElement[] stackTraceElements = throwable.getStackTrace();
for (StackTraceElement stackTraceElement : stackTraceElements) {
// className, fileName, methodName, lineName
stackTraceElement.getClassName();
stackTraceElement.getFileName();
stackTraceElement.getMethodName();
stackTraceElement.getLineNumber();
stackTraceElement.toString();
}
StackTraceElement 類能夠獲得類名(包含包路徑)稚配,文件名畅涂,方法名及代碼行號,而toString方法則會產(chǎn)生一個格式化的字符串道川,就是上面例子中的一行異常信息午衰。
而靜態(tài)的Thread.getAllStackTrace
方法則可以產(chǎn)生所有線程的堆棧跟蹤:
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread t : map.keySet()) {
StackTraceElement[] stackTraceElements = map.get(t);
// doSomething
}
三、總結(jié)
1. 注意事項
- 異常處理機(jī)制的一個目標(biāo)冒萄,將正常處理與錯誤處理分開臊岸;比如說展示給用戶的錯誤信息盡量不要和程序混淆到一塊,可以使用枚舉或者配置文件統(tǒng)一管理錯誤信息尊流;
- 只在有必要的時候才使用異常帅戒,不要用異常來控制程序的流程,因為異常使用過多會影響程序的性能崖技,能用代碼解決的異常就不要拋出逻住;
- 不要只拋出RuntimeException,應(yīng)該尋找更加適當(dāng)?shù)淖宇惢騽?chuàng)建自己的異常類迎献;不要只捕獲Throwable異常瞎访,會使代碼不太容易維護(hù)舵抹;
- 不要使用空的catch塊为朋,如果我們想忽略掉異常钞它,可以在catch塊中添加日志妆绞,這樣假如這里出現(xiàn)了問題可以及時排查到沧烈;
- 避免多次在日志信息中記錄同一個異常他去,一般情況下異常都是往上層拋出荸百,如果每次拋出都log一下的話稽坤,則不利于我們定位到問題所在咕幻;
- 異常的拋出與捕獲可以遵循“早拋出渔伯,晚捕獲”這種規(guī)則;
2. 異常所能解決的問題
我們之所以使用異常肄程,在于異陈嗪穑可以解決如下問題:
- 出了什么問題选浑;
- 在哪里出的問題;
- 為什么會出問題玄叠;
在有效使用異常的情況下古徒,異常類型回答了“什么錯誤”被拋出,異常堆棧跟蹤回答了“在哪里”拋出读恃,異常信息回答了“為什么”會拋出隧膘,所以我們在進(jìn)行拋出或捕獲異常的時候,要能準(zhǔn)備的解決這些問題寺惫。
3. try/catch/finally中包含return的執(zhí)行順序問題
當(dāng)try/catch/finally中包含return語句的時候疹吃,很有意思。這個問題以前專門測試過西雀,不過后來忘記記錄到哪了萨驶,這次記錄下,目前可以分為以下幾種情況:
3.1 try有return艇肴,那么finally里的代碼會不會執(zhí)行腔呜,在try的return前還是后執(zhí)行?
public static void main(String[] args) {
System.out.println(returnTest());
}
public static boolean returnTest() {
try {
System.out.println("try塊");
return true;
} finally {
System.out.println("finally塊");
}
}
/*
try塊
finally塊
true
*/
這個問題就比較簡單了再悼,finally里的代碼一定會執(zhí)行核畴,并且是在try的return前執(zhí)行;
3.2 try有return冲九,finally也有return膛檀,那么執(zhí)行結(jié)果是?
// main方法省略
public static boolean returnTest() {
try {
System.out.println("try塊");
return true;
} finally {
System.out.println("finally塊");
return false;
}
}
/*
try塊
finally塊
false
*/
首先娘侍,finally塊代碼一定會執(zhí)行咖刃,可以看到,finally里的return覆蓋掉了try里的return憾筏;
3.3 try后面有catch嚎杨,catch里也有return,怎么執(zhí)行氧腰?
// main方法省略
public static boolean returnTest() {
try {
System.out.println("try塊");
int temp = 23 / 0;
return true;
} catch (Exception e) {
System.out.println("catch塊");
return false;
} finally {
System.out.println("finally塊");
}
}
/*
try塊
catch塊
finally塊
false
*/
可以看到枫浙,在流程都執(zhí)行完成之后,catch塊中的return覆蓋了try塊的return古拴;接下來如果給finally也加上return的話箩帚,可以看下執(zhí)行結(jié)果:
// main方法省略
public static boolean returnTest() {
try {
System.out.println("try塊");
int temp = 23 / 0;
return true;
} catch (Exception e) {
System.out.println("catch塊");
return true;
} finally {
System.out.println("finally塊");
return false;
}
}
/*
try塊
catch塊
finally塊
false
*/
可以看到,首先finally里的代碼一定會執(zhí)行黄痪,并且如果finally里沒有return語句紧帕,而catch里有return語句,則catch里的return語句會覆蓋掉try的;而如果finally里也有return語句是嗜,則finally里的return語句會覆蓋掉前面的愈案。
3.4 數(shù)字相加問題
直接看代碼:
public static int returnTest() {
int temp = 23;
try {
System.out.println("try塊");
return temp += 88;
} catch (Exception e) {
System.out.println("catch塊");
} finally {
if (temp > 25) {
System.out.println("temp>25:" + temp);
}
System.out.println("finally塊");
}
return temp;
}
大家可以先猜一下結(jié)果,然后再看結(jié)果:
try塊
temp>25:111
finally塊
111
從表面上看鹅搪,temp的值先變?yōu)榱?11站绪,然后再執(zhí)行的finally,那是不是try里先return了丽柿,再執(zhí)行的finally呢恢准?其實,并不是try語句中return執(zhí)行完之后才執(zhí)行的finally甫题,而是在執(zhí)行return temp+=88
時馁筐,分成了兩步,先temp+=88;
再return temp;
幔睬,如果我們將return temp;
放到System.out.println("finally塊");
后面眯漩,則輸出結(jié)果不變芹扭;我們來修改下finally語句:
} finally {
if (temp > 25) {
System.out.println("temp>25:" + temp);
}
System.out.println("finally塊");
temp = 100;
}
因為finally沒有return麻顶,那么不管你是不是改變了要返回的那個變量,返回的值依然不變舱卡。
3.5 總結(jié)
可以來簡單總結(jié)下:
- 當(dāng)包含finally語句時辅肾,無論try/catch有沒有return語句,finally塊中的代碼一定會執(zhí)行轮锥;
- 當(dāng)finally塊中也包含return語句時矫钓,finally塊的return會覆蓋掉try/catch中的return語句;
本文參考自:
《Java核心技術(shù) 卷I》
海子 - Java異常處理和設(shè)計
知乎 - Java中如何優(yōu)雅的處理異常
IBM - Java 異常處理的誤區(qū)和經(jīng)驗總結(jié)
Java 8 Exceptions