Java 編程時(shí),總會(huì)遇到可預(yù)見(jiàn)或不可預(yù)知的異常情況获搏,程序如何處理好這些異常是保證程序穩(wěn)定健壯的無(wú)比重要。對(duì)于 Java,通過(guò) Throwable
類(lèi)的眾多子類(lèi)來(lái)描述程序遇到的各類(lèi)異常项贺,主要分為 Exception
和 Error
。
-
Error
: 一般指虛擬機(jī)相關(guān)問(wèn)題峭判,如系統(tǒng)崩潰开缎,內(nèi)存不足,調(diào)用棧溢出等嚴(yán)重問(wèn)題林螃,需要程序終止解決奕删。 -
Exception
:程序可預(yù)測(cè)解決的異常,如訪問(wèn)異常文件導(dǎo)致的 IO 異常疗认,或者用戶(hù)自定義的異常完残,此類(lèi)異常通過(guò)合理處理,可不造成程序中斷横漏,保障了程序健壯性谨设。Exception
中最主要的,又分為:- 檢查異常(checked exception):若拋出此異常绊茧,方法后必須強(qiáng)制以
throws
關(guān)鍵字進(jìn)行拋出聲明铝宵,例如IOException
等 - 非檢查異常(unchecked exception):無(wú)需拋出聲明,
RuntimeException
均屬于非檢查異常,例如NullPointerException
,IndexOutOfBoundsException
等鹏秋。
- 檢查異常(checked exception):若拋出此異常绊茧,方法后必須強(qiáng)制以
對(duì)于如何處理異常尊蚁,Java 采用的方法就是 try...catch...finally
。try
所括代碼侣夷,稱(chēng)為監(jiān)控區(qū)域(guarded region)横朋,該區(qū)域內(nèi)所有代碼拋出的異常,會(huì)被 catch
中匹配的 Exception
分支所捕獲百拓,此處父類(lèi)可捕獲子類(lèi)異常琴锭,最后在 finally
中處理收尾。
try...catch...finally
本身不難理解衙传,但原理上也有一些需要注意的地方决帖,下文也總結(jié)了些使用技巧。
原理剖析
討論 try...catch...finally
的原理蓖捶,最主要的是分析 throw
和 return
的最終狀態(tài)地回。以下段代碼舉例:
例一:
void test1() {
try {
test2();
} catch (Exception e) {
throw new RuntimeException("test1 - catch");
} finally {
throw new RuntimeException("test1 - finally");
}
}
void test2() {
throw new RuntimeException("test2");
}
public static void main(String[] args) {
Main main = new Main();
main.test1();
}
最終,這段代碼返回的會(huì)是那一個(gè)異常呢俊鱼?答案是 finally
中的那句
java.lang.RuntimeException: test1 - finally
at com.example.Main.test1(Main.java:20)
at com.example.Main.main(Main.java:30)
再來(lái)看刻像,若 try...catch...finally
中,每個(gè)代碼塊均含有 return
并闲,那么返回什么呢细睡?看下例:
例二:
int test1() {
int i = 0;
try {
System.out.println("test1 - " + ++i);
test2(i);
System.out.println("test1 - " + ++i);
return ++i;
} catch (Exception e) {
System.out.println("test1 - catch - " + ++i);
return i + 10;
} finally {
System.out.println("test1 - finally - " + ++i);
return i + 100;
}
}
void test2(int i) {
System.out.println("test2 - " + i);
throw new RuntimeException("test2");
}
public static void main(String[] args) {
Main main = new Main();
System.out.println(main.test1());
}
返回如下,結(jié)果是 finally 中的返回值帝火。
test1 - 1
test2 - 1
test1 - catch - 2
test1 - finally - 3
103
可以注意到溜徙, <u>finally
語(yǔ)句塊是在控制轉(zhuǎn)移語(yǔ)句之前執(zhí)行的</u>,控制轉(zhuǎn)移語(yǔ)句有 throw
, return
犀填。
這是因?yàn)槊染?Java 虛擬機(jī)編譯 finally
語(yǔ)句塊時(shí),會(huì)把 finally
語(yǔ)句塊作為子程序宏浩,直接插入到 try
語(yǔ)句塊或者 catch
語(yǔ)句塊的控制轉(zhuǎn)移語(yǔ)句之前知残。
除了執(zhí)行順序,還有一點(diǎn)不可忽視比庄,就是在例一求妹、例二中,程序最終獲得的是 finally
中拋出的異常以及返回值佳窑。這是因?yàn)橹苹校趫?zhí)行 finally
之前,try
或者 catch
語(yǔ)句塊會(huì)將其返回值保存到本地變量表(Local Variable Table)中神凑。待 finally
執(zhí)行完畢之后净神,再恢復(fù)保留的返回值到操作數(shù)棧中何吝,然后通過(guò) return
或者 throw
語(yǔ)句將其返回給該方法的調(diào)用者(invoker)。
那么鹃唯,若出現(xiàn)控制轉(zhuǎn)移語(yǔ)句的沖突時(shí)爱榕,以誰(shuí)為準(zhǔn)呢?我們是無(wú)法在一個(gè)塊語(yǔ)句中坡慌,同時(shí)定義 return
和 throw
的黔酥,編譯器會(huì)提示錯(cuò)誤,因?yàn)檫@兩條語(yǔ)句是無(wú)法都執(zhí)行的洪橘。這里所指的沖突跪者,是當(dāng) try...catch
中的控制轉(zhuǎn)移語(yǔ)句與 finally
中的同時(shí)出現(xiàn)了怎么辦?
看以下例子:
例三:
以下代碼會(huì)正常返回熄求,不會(huì)拋出異常渣玲。
int test1() {
int i = 0;
try {
System.out.println("test1 - " + ++i);
test2(i); // 同上
System.out.println("test1 - " + ++i);
return ++i;
} catch (Exception e) {
System.out.println("test1 - catch - " + ++i);
throw new RuntimeException("test1 - catch");
} finally {
System.out.println("test1 - finally - " + ++i);
return i + 10;
}
}
test1 - 1
test2 - 1
test1 - catch - 2
test1 - finally - 3
13
以下代碼會(huì)拋出異常
int test1() {
int i = 0;
try {
System.out.println("test1 - " + ++i);
test2(i); // 同上
System.out.println("test1 - " + ++i);
return ++i;
} catch (Exception e) {
System.out.println("test1 - catch - " + ++i);
return i + 10;
} finally {
System.out.println("test1 - finally - " + ++i);
throw new RuntimeException("test1 - finally");
}
}
test1 - 1
test2 - 1
test1 - catch - 2
test1 - finally - 3
Exception in thread "main" java.lang.RuntimeException: test1 - finally
at com.example.Main.test1(Main.java:26)
at com.example.Main.main(Main.java:37)
根據(jù)例三,可以發(fā)現(xiàn)弟晚,當(dāng)控制轉(zhuǎn)移同時(shí)出現(xiàn)時(shí)柜蜈,是以 finally
中的為準(zhǔn)的,無(wú)論該控制轉(zhuǎn)移是 return
還是throw
指巡。
以上幾例可以看到,finally
中的控制轉(zhuǎn)移語(yǔ)句會(huì)影響到返回值和返回的異常棧隶垮,那若 finally
不含 return
和 throw
呢藻雪?會(huì)對(duì)結(jié)果產(chǎn)生什么影響呢?看看例四:
int test1() {
int i = 0;
try {
System.out.println("test1 - " + ++i);
test2(i);
System.out.println("test1 - " + ++i);
return ++i;
} catch (Exception e) {
System.out.println("test1 - catch - " + ++i);
return i;
} finally {
System.out.println("test1 - finally - " + ++i);
}
}
test1 - 1
test2 - 1
test1 - catch - 2
test1 - finally - 3
2
可見(jiàn)狸吞,finally
中 i
已經(jīng)是 3 了勉耀,但返回值還是 2。這是因?yàn)?finally
中的控制轉(zhuǎn)移語(yǔ)句會(huì)修改本地變量表中的返回值和異常棧蹋偏,但其他情況便斥,是無(wú)法修改已經(jīng)保存在本地變量表中的返回值和異常棧的,因此威始,finally
中對(duì) i
的變更枢纠,不會(huì)體現(xiàn)在返回值上。這是需要注意的黎棠!
根據(jù)以上實(shí)例晋渺,可以總結(jié)到:
-
finally
語(yǔ)句塊是在控制轉(zhuǎn)移語(yǔ)句(僅針對(duì)try...catch...finally
塊而言,塊外的程序轉(zhuǎn)移不在討論范圍之內(nèi))之前執(zhí)行的脓斩,控制轉(zhuǎn)移語(yǔ)句有throw
,return
- 在執(zhí)行
finally
之前木西,程序會(huì)將try
或者catch
中的返回值和異常棧存入本地變量表 - 若
finally
中無(wú)控制轉(zhuǎn)移語(yǔ)句(return 和 throw),則程序返回之前本地變量表中的返回值和異常棧随静;- 需要注意的是八千,若
finally
中無(wú)控制轉(zhuǎn)移語(yǔ)句吗讶,那么即使在finally
中變更返回的變量的值,是不會(huì)影響返回值的恋捆。 - 若
try...catch
也不涉及控制轉(zhuǎn)移語(yǔ)句照皆,程序?qū)㈨樞驁?zhí)行,finally
中對(duì)方法內(nèi)變量的變更均有效
- 需要注意的是八千,若
- 若
finally
中含有控制轉(zhuǎn)移語(yǔ)句鸠信,則以finally
中的控制轉(zhuǎn)移語(yǔ)句為準(zhǔn)纵寝,即無(wú)論finally
中含有return
還是throw
,均以該語(yǔ)句為準(zhǔn)星立,會(huì)覆蓋原本地變量表中的返回值或異常棧的內(nèi)容爽茴。
最佳實(shí)踐
Java 的異常處理其實(shí)并不難,明白后總結(jié)了以下幾點(diǎn)實(shí)踐經(jīng)驗(yàn)绰垂。
準(zhǔn)確定義
盡可能準(zhǔn)確匹配的定義捕獲異常室奏,不要一刀切的處理。這樣會(huì)掩蓋諸多開(kāi)發(fā)時(shí)未意識(shí)到的問(wèn)題劲装,這是非常危險(xiǎn)的胧沫。
Tip1: 永遠(yuǎn)不要直接 catch(Throwable e)
Java 異常中的 Error
也繼承 Throwable
,若直接捕獲 Throwable
要么會(huì)掩蓋一些 JVM 造成的錯(cuò)誤占业,又或者造成代碼無(wú)法按計(jì)劃執(zhí)行(有些 JVM 錯(cuò)誤不會(huì)被 catch
捕獲绒怨,和開(kāi)發(fā)人員預(yù)想邏輯相違背)。
Tip2: 準(zhǔn)確 throws
檢查異常
當(dāng)需要 throws
時(shí)谦疾,不要將異常定義過(guò)泛南蹂,定義過(guò)泛會(huì)破壞檢查異常的意義。若直接 throws Exception
念恍,那么代碼如果需要拋出其他的檢查異常六剥,上層調(diào)用永遠(yuǎn)無(wú)法知道
// 不推薦
void test() throws Exception {}
// Correct!
void test() throws SpecException1, SpecException2 {}
Tip3: 明確 catch
的異常類(lèi)型
當(dāng)需要 catch
時(shí),需要明確捕獲異常類(lèi)型峰伙。若只是泛泛的 catch(Exception e)
疗疟,會(huì)造成:
- 若底層重構(gòu),拋出其他類(lèi)別的異常時(shí)瞳氓,也會(huì)被簡(jiǎn)單的捕獲策彤,無(wú)法被上層感知
- 模糊了程序邏輯,掩蓋了可能存在的未被開(kāi)發(fā)人員考慮到的問(wèn)題
// 不推薦
try {
// do something
} catch (Exception e) {}
// 推薦
try {
// do something
} catch (SpecException1 e) {
} catch (SpecException2 e) {}
妥善處理
異常棧包含著豐富的信息匣摘,幫助開(kāi)發(fā)人員定位問(wèn)題锅锨。
因此最佳的異常棧,應(yīng)該由問(wèn)題發(fā)生處拋出恋沃,不應(yīng)該被肆意的覆蓋或者“吞食”必搞;拋出的異常棧,需要合理的輸出囊咏,能妥善的告知開(kāi)發(fā)人員進(jìn)行問(wèn)題的定位恕洲。
Tip4:早 throw
晚 catch
編碼時(shí)塔橡,應(yīng)該盡早拋出異常,并在有足夠信息后再捕獲異常進(jìn)行妥善處理霜第。
如果有些異常暫時(shí)無(wú)法處理葛家,不要為了catch
而catch
,而應(yīng)該繼續(xù) throw
泌类。
Tip5: 吞食有害 harmful if swallowed
《Java 編程思想》中提到癞谒,“被檢查的異常” 的處理方法是方法后面跟著 throws 顯式聲明的異常刃榨。這會(huì)強(qiáng)制讓開(kāi)發(fā)人員在未就緒時(shí)處理這個(gè)錯(cuò)誤弹砚,有時(shí)開(kāi)發(fā)人員為了“取巧”,經(jīng)常會(huì) swallow it枢希,這不是太好的設(shè)計(jì)桌吃。所謂 “swallow” 是如下代碼
void test() {
try{
method(); // throws checked exception
} catch (Exception e) {
System.out.println("exception"); // exception 被“吞”,異常棧不再能被追溯
}
}
此時(shí)苞轿,檢查的異常被不合理的處理了茅诱,會(huì)導(dǎo)致難以排查問(wèn)題。
若出現(xiàn)暫時(shí)不想處理搬卒,不要隨意的用 try...catch...finally
進(jìn)行處理瑟俭,可以有兩種辦法,一是可以將異常包入 RuntimeException()
中處理:
void test() {
try{
method();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
二是繼續(xù) throws
拋出契邀,由更上層調(diào)用進(jìn)行處理摆寄。
Tip6:維護(hù)異常棧信息,切勿輕易丟棄
有時(shí)開(kāi)發(fā)人員會(huì)自定義異常類(lèi)蹂安,切記合理包裝異常棧,不要輕易丟棄锐帜。如下田盈,自定義的 MyException
異常類(lèi),構(gòu)造函數(shù)允許僅接收字符串缴阎,但在 throw new MyException(e.getMessage())
時(shí)允瞧,e.getMessage()
會(huì)丟失異常棧信息。
class MyException extends RuntimeException {
MyException(String msg) {
super(msg);
}
MyException(Throwable e) {
super(e);
}
}
void test() {
try {
// do something
} catch (Exception e) {
// 不推薦
// throw new MyException(e.getMessage());
// 推薦
throw new MyException(e);
}
}
又或者底層方法拋出了異常 SpecException1
蛮拔,但在上層調(diào)用捕獲后述暂,開(kāi)發(fā)人員又以 SpecException2
拋出了。那么就丟失了 SpecException1
拋出時(shí)的異常棧建炫,問(wèn)題定位就不夠準(zhǔn)確了畦韭。若有額外補(bǔ)充異常信息的需求,也請(qǐng)將異常棧一同傳遞肛跌。如下舉例
// 不推薦
try {
// do something
} catch (SpecException1 e) {
throw new SpecException2("some info");
}
// 推薦
try {
// do something
} catch (SpecException1 e) {
throw new SpecException2("some info", e);
}
Tip7: 過(guò)猶不及艺配,一條異常不要輸出兩遍察郁。要么記錄,要么拋出转唉,不要一起執(zhí)行
tip6 告誡不要輕易丟棄異常棧皮钠,但是這一條告誡也不要過(guò)多的輸出異常信息。
這是因?yàn)檫^(guò)多的輸出赠法,會(huì)對(duì)開(kāi)發(fā)人員造成混淆麦轰,不利于日志的分析(往往是自動(dòng)化進(jìn)行)。當(dāng)異常信息過(guò)多時(shí)砖织,還要去分辨是不是同一個(gè)異常造成的款侵,太浪費(fèi)時(shí)間了。
簡(jiǎn)單的講镶苞,一條異常不應(yīng)該輸出多遍喳坠。開(kāi)發(fā)人員一般不會(huì)在同一代碼塊中多次輸出異常。但可能會(huì)有以下情況:
// 不推薦
try {
// do something
} catch (SpecException1 e) {
logger.debug("exception: " + e);
throw e;
}
以上代碼茂蚓,既對(duì)異常進(jìn)行了日志輸出壕鹉,又再一次拋出了異常。
拋出異常的目的聋涨,是為了被上層調(diào)用捕獲晾浴,由于上層調(diào)用并不知底層調(diào)用已經(jīng)對(duì)異常進(jìn)行了輸出(底層封裝和上層調(diào)用并非由同一開(kāi)發(fā)人員完成),往往會(huì)在此對(duì)異常進(jìn)行再次的輸出牍白。
又或者脊凰,由框架默認(rèn)統(tǒng)一處理了拋出的異常∶龋總之狸涌,造成異常輸出兩遍。
這需要避免最岗。
Tip8: 異常應(yīng)由一行日志代碼輸出
將一個(gè)異常帕胆,分多條日志輸出。日志不多時(shí)般渡,可能還可以保證兩條日志的連續(xù)性懒豹。但服務(wù)往往是多線程,日志也可能歸集了分布式服務(wù)的信息驯用,這造成代碼中連續(xù)的輸出脸秽,實(shí)際在日志文件中相隔成千上萬(wàn)行,難以排查問(wèn)題蝴乔。
因此建議记餐,將異常在一條日志代碼中輸出。
// 不推薦
try {
// do something
} catch (SpecException1 e) {
logger.debug("exception: " + e.getMessage());
logger.debug("trace: " + e);
}
// 推薦
try {
// do something
} catch (SpecException1 e) {
logger.debug("exception: " + e.getMessage() + "薇正, trace: " + e);
}
Tip9: 不要只是簡(jiǎn)單的打印異常
不要只是簡(jiǎn)單的將異常打印剥扣。如果是調(diào)用的方法巩剖,簡(jiǎn)單的打印異常,上層調(diào)用并無(wú)法感知钠怯,而認(rèn)為調(diào)用正確佳魔,這會(huì)造成更多的異常發(fā)生。
一定要妥善處理晦炊。
如果異常拋出到最上層鞠鲜,那么可以打印,但也不要直接將異常直接拋給用戶(hù)断国。因?yàn)檫@樣的信息贤姆,對(duì)用戶(hù)而言是沒(méi)有任何意義的,甚至可能暴露了系統(tǒng)的問(wèn)題稳衬,給攻擊者可乘之機(jī)霞捡。
因此,可以在系統(tǒng)最上層調(diào)用中薄疚,統(tǒng)一打印異常碧信,并將異常進(jìn)行封裝,轉(zhuǎn)換為用戶(hù)可理解的錯(cuò)誤信息街夭。
關(guān)注 finally
Tip10: finally
中不要return
和 throw
看了之前的原理剖析砰碴,可以知道 finally
中的 return
和 throw
會(huì)覆蓋 try...catch
中的值。
因此不建議在 finally
中 return
或 throw
板丽。但有時(shí)呈枉,throw
會(huì)比較隱蔽,例如以下代碼埃碱,method2
可能在調(diào)用是拋出異常猖辫,若不處理,就會(huì)覆蓋 method1
拋出的異常砚殿。因此啃憎,需要用 try...catch...finally
再次包一下。
// 不推薦
try {
method1();
} finally {
throw new MyException();
}
// 需關(guān)注 method2
try {
method1();
} finally {
method2();
}
// 推薦
try {
method1();
} finally {
try {
method2();
} catch (SpecException e) {
// do something
} finally {
// do something
}
}
Tip11: 記得在 finally
中釋放資源
記得在 finally
中釋放資源瓮具,避免資源浪費(fèi)荧飞。一般是釋放管道凡人、連接等名党。
或者使用 Java 7 的寫(xiě)法:
try(open the resouces) {
// do something
}
其他注意點(diǎn)
Tip12: 不要將 try...catch...finally
作為流程控制
這會(huì)導(dǎo)致代碼混亂不堪,難以閱讀挠轴,重構(gòu)困難传睹。異常處理不是這么用的!為了同事的發(fā)際線岸晦,請(qǐng)珍惜這段緣欧啤。
Tip13: 巧妙的使用模板代碼睛藻,避免 try...catch...finally
的冗余
常見(jiàn)的是文件的開(kāi)啟關(guān)閉,數(shù)據(jù)庫(kù)連接的開(kāi)啟和關(guān)閉等邢隧。例如:
class DBUtil{
public static void closeConnection(Connection conn){
try{
conn.close();
} catch(Exception ex){
//Log Exception - Cannot close connection
}
}
}
public void dataAccessCode() {
Connection conn = null;
try{
conn = getConnection();
// do something
} finally{
DBUtil.closeConnection(conn);
}
}
Tip14: 異常對(duì)性能的影響
處理異常對(duì) JVM 而言店印,是比較消耗性能的,因?yàn)樾枰~外的去維護(hù)異常棧倒慧。
調(diào)用一個(gè)拋出異常的方法的資源消耗按摘,要比調(diào)用一個(gè)一般方法多。
因此纫谅,需要平衡好異常拋出的層級(jí)炫贤,避免過(guò)多層級(jí)的異常棧傳遞。更要注意付秕,在循環(huán)中的異常兰珍。
Tip15: JavaDoc 注釋說(shuō)明
注釋不規(guī)范,同事淚兩行询吴。
雖然我覺(jué)得優(yōu)秀的程序員寫(xiě)的清晰富有邏輯的代碼掠河,足以說(shuō)明代碼所解決的問(wèn)題。但實(shí)際生產(chǎn)中汰寓,往往是過(guò)高的要求了口柳。所以,還是寫(xiě)好代碼注釋吧有滑。
參考 JDK 代碼注釋來(lái)寫(xiě)跃闹,使用 @throws
,例如以下是 java.io.File.java
中的一段
/**
* Atomically creates a new, empty file named ...
*
* @return <code>true</code> if the named file does not exist and was
* successfully created; <code>false</code> if the named file
* already exists
*
* @throws IOException
* If an I/O error occurred
*
* @throws SecurityException
* If a security manager exists and its <code>{@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}</code>
* method denies write access to the file
*
* @since 1.2
*/
public boolean createNewFile() throws IOException {}
總結(jié)
Java 的異常處理毛好,使用并不困難望艺,難點(diǎn)在于實(shí)踐中的把握。
理解了 finally
原理肌访,記住早 throw
晚 catch
的準(zhǔn)則找默,有助于幫助提高代碼質(zhì)量,提高排查問(wèn)題的效率吼驶。
最佳實(shí)踐是我參考網(wǎng)上文章惩激,加之以總結(jié)的結(jié)果,隨著日后的實(shí)踐蟹演,會(huì)逐漸補(bǔ)充风钻。
參考資料
[1] 《Java 編程思想》
[2] 關(guān)于JAVA異常處理的20個(gè)最佳實(shí)踐,作者超人歸來(lái)酒请, https://segmentfault.com/a/1190000015028573
[3] 如何優(yōu)雅的處理異常(java)骡技?知乎網(wǎng)友,https://www.zhihu.com/question/28254987
[4] Java 異常處理的誤區(qū)和經(jīng)驗(yàn)總結(jié),作者趙愛(ài)兵布朦,https://www.ibm.com/developerworks/cn/java/j-lo-exception-misdirection/index.html
[5] 關(guān)于 Java 中 finally 語(yǔ)句塊的深度辨析囤萤,作者魏成利, https://www.ibm.com/developerworks/cn/java/j-lo-finally/index.html
[6] 深入理解java異常處理機(jī)制是趴,作者規(guī)速涛舍,https://blog.csdn.net/hguisu/article/details/6155636
[7] Top 11 Java Exception Best Practices, 作者 Krishna Srinivasan唆途,https://javabeat.net/java-exception-best-practices/