作者:allen4tech 慕課網(wǎng)
JAVA 異常類(lèi)型結(jié)構(gòu)
Throwable 是所有異常類(lèi)型的基類(lèi),Throwable 下一層分為兩個(gè)分支,Error 和 Exception.

Error 和 Exeption
- Error
Error 描述了 JAVA 程序運(yùn)行時(shí)系統(tǒng)的內(nèi)部錯(cuò)誤,通常比較嚴(yán)重,除了通知用戶(hù)和盡力使應(yīng)用程序安全地終止之外,無(wú)能為力,應(yīng)用程序不應(yīng)該嘗試去捕獲這種異常。通常為一些虛擬機(jī)異常,如 StackOverflowError 等。
- Exception
Exception 類(lèi)型下面又分為兩個(gè)分支,一個(gè)分支派生自 RuntimeException,這種異常通常為程序錯(cuò)誤導(dǎo)致的異常;另一個(gè)分支為非派生自 RuntimeException 的異常,這種異常通常是程序本身沒(méi)有問(wèn)題,由于像 I/O 錯(cuò)誤等問(wèn)題導(dǎo)致的異常,每個(gè)異常類(lèi)用逗號(hào)隔開(kāi)。
受查異常和非受查異常
- 受查異常
受查異常會(huì)在編譯時(shí)被檢測(cè)。如果一個(gè)方法中的代碼會(huì)拋出受查異常,則該方法必須包含異常處理,即 try-catch 代碼塊,或在方法簽名中用 throws 關(guān)鍵字聲明該方法可能會(huì)拋出的受查異常,否則編譯無(wú)法通過(guò)。如果一個(gè)方法可能拋出多個(gè)受查異常類(lèi)型,就必須在方法的簽名處列出所有的異常類(lèi)。
通過(guò) throws 關(guān)鍵字聲明可能拋出的異常

try-catch 處理異常

- 非受查異常
非受查異常不會(huì)在編譯時(shí)被檢測(cè)。JAVA 中 Error 和 RuntimeException 類(lèi)的子類(lèi)屬于非受查異常,除此之外繼承自 Exception 的類(lèi)型為受查異常。
異常的拋出與捕獲
直接拋出異常
通常,應(yīng)該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續(xù)傳遞下去。傳遞異??梢栽诜椒ê灻幨褂?strong> throws 關(guān)鍵字聲明可能會(huì)拋出的異常。

封裝異常再拋出
有時(shí)我們會(huì)從 catch 中拋出一個(gè)異常,目的是為了改變異常的類(lèi)型。多用于在多系統(tǒng)集成時(shí),當(dāng)某個(gè)子系統(tǒng)故障,異常類(lèi)型可能有多種,可以用統(tǒng)一的異常類(lèi)型向外暴露,不需暴露太多內(nèi)部異常細(xì)節(jié)。

捕獲異常
在一個(gè) try-catch 語(yǔ)句塊中可以捕獲多個(gè)異常類(lèi)型,并對(duì)不同類(lèi)型的異常做出不同的處理

同一個(gè) catch 也可以捕獲多種類(lèi)型異常,用 | 隔開(kāi)

自定義異常
習(xí)慣上,定義一個(gè)異常類(lèi)應(yīng)包含兩個(gè)構(gòu)造函數(shù),一個(gè)無(wú)參構(gòu)造函數(shù)和一個(gè)帶有詳細(xì)描述信息的構(gòu)造函數(shù)(Throwable 的 toString 方法會(huì)打印這些詳細(xì)信息,調(diào)試時(shí)很有用)

try-catch-finally
當(dāng)方法中發(fā)生異常,異常處之后的代碼不會(huì)再執(zhí)行,如果之前獲取了一些本地資源需要釋放,則需要在方法正常結(jié)束時(shí)和 catch 語(yǔ)句中都調(diào)用釋放本地資源的代碼,顯得代碼比較繁瑣,finally 語(yǔ)句可以解決這個(gè)問(wèn)題。

調(diào)用該方法時(shí),讀取文件時(shí)若發(fā)生異常,代碼會(huì)進(jìn)入 catch 代碼塊,之后進(jìn)入 finally 代碼塊;若讀取文件時(shí)未發(fā)生異常,則會(huì)跳過(guò) catch 代碼塊直接進(jìn)入 finally 代碼塊。所以無(wú)論代碼中是否發(fā)生異常,fianlly 中的代碼都會(huì)執(zhí)行。
若 catch 代碼塊中包含 return 語(yǔ)句,finally 中的代碼還會(huì)執(zhí)行嗎?將以上代碼中的 catch 子句修改如下:

調(diào)用 readFile 方法,觀察當(dāng) catch 子句中調(diào)用 return 語(yǔ)句時(shí),finally 子句是否執(zhí)行

可見(jiàn),即使 catch 中包含了 return 語(yǔ)句,finally 子句依然會(huì)執(zhí)行。若 finally 中也包含 return 語(yǔ)句,finally 中的 return 會(huì)覆蓋前面的 return.
try-with-resource
上面例子中,finally 中的 close 方法也可能拋出 IOException, 從而覆蓋了原始異常。JAVA 7 提供了更優(yōu)雅的方式來(lái)實(shí)現(xiàn)資源的自動(dòng)釋放,自動(dòng)釋放的資源需要是實(shí)現(xiàn)了 AutoCloseable 接口的類(lèi)。

try 代碼塊退出時(shí),會(huì)自動(dòng)調(diào)用 scanner.close 方法,和把 scanner.close 方法放在 finally 代碼塊中不同的是,若 scanner.close 拋出異常,則會(huì)被抑制,拋出的仍然為原始異常。被抑制的異常會(huì)由 addSusppressed 方法添加到原來(lái)的異常,如果想要獲取被抑制的異常列表,可以調(diào)用 getSuppressed 方法來(lái)獲取。
阿里巴巴異常處理規(guī)約
1、【強(qiáng)制】 Java 類(lèi)庫(kù)中定義的可以通過(guò)預(yù)檢查方式規(guī)避的 RuntimeException 異常不應(yīng)該通過(guò)catch 的方式來(lái)處理,比如:NullPointerException, IndexOutOfBoundsException 等等。
說(shuō)明:無(wú)法通過(guò)預(yù)檢查的異常除外,比如,在解析字符串形式的數(shù)字時(shí),不得不通過(guò) catch NumberFormatException 來(lái)實(shí)現(xiàn)。
正例:if (obj != null) {…}
反例:try { obj.method(); } catch (NullPointerException e) {…}
2、【強(qiáng)制】 異常不要用來(lái)做流程控制,條件控制。
說(shuō)明: 異常設(shè)計(jì)的初衷是解決程序運(yùn)行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多
3、【強(qiáng)制】 catch 時(shí)請(qǐng)分清穩(wěn)定代碼和非穩(wěn)定代碼,穩(wěn)定代碼指的是無(wú)論如何不會(huì)出錯(cuò)的代碼。對(duì)于非穩(wěn)定代碼的 catch 盡可能進(jìn)行區(qū)分異常類(lèi)型,再做對(duì)應(yīng)的異常處理。
說(shuō)明: 對(duì)大段代碼進(jìn)行 try-catch,使程序無(wú)法根據(jù)不同的異常做出正確的應(yīng)激反應(yīng),也不利于定位問(wèn)題,這是一種不負(fù)責(zé)任的表現(xiàn)。
正例: 用戶(hù)注冊(cè)的場(chǎng)景中,如果用戶(hù)輸入非法字符, 或用戶(hù)名稱(chēng)已存在, 或用戶(hù)輸入密碼過(guò)于簡(jiǎn)單,在程序上作出分門(mén)別類(lèi)的判斷,并提示給用戶(hù)。
4、【強(qiáng)制】 捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請(qǐng)將該異常拋給它的調(diào)用者。最外層的業(yè)務(wù)使用者,必須處理異常,將其轉(zhuǎn)化為用戶(hù)可以理解的內(nèi)容。
5、【強(qiáng)制】 有 try 塊放到了事務(wù)代碼中, catch 異常后,如果需要回滾事務(wù),一定要注意手動(dòng)回滾事務(wù)。
6、【強(qiáng)制】 finally 塊必須對(duì)資源對(duì)象、流對(duì)象進(jìn)行關(guān)閉,有異常也要做 try-catch。
說(shuō)明: 如果 JDK7 及以上,可以使用 try-with-resources 方式。
7、【強(qiáng)制】 不要在 finally 塊中使用 return。
說(shuō)明:finally 塊中的 return 返回后方法結(jié)束執(zhí)行,不會(huì)再執(zhí)行 try 塊中的 return 語(yǔ)句。
8、【強(qiáng)制】 捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類(lèi)。
說(shuō)明: 如果預(yù)期對(duì)方拋的是繡球,實(shí)際接到的是鉛球,就會(huì)產(chǎn)生意外情況。
9、【推薦】 方法的返回值可以為 null,不強(qiáng)制返回空集合,或者空對(duì)象等,必須添加注釋充分說(shuō)明什么情況下會(huì)返回 null 值。
說(shuō)明: 本手冊(cè)明確防止 NPE 是調(diào)用者的責(zé)任。即使被調(diào)用方法返回空集合或者空對(duì)象,對(duì)調(diào)用者來(lái)說(shuō),也并非高枕無(wú)憂(yōu),必須考慮到遠(yuǎn)程調(diào)用失敗、 序列化失敗、 運(yùn)行時(shí)異常等場(chǎng)景返回null 的情況。
10、【推薦】 防止 NPE,是程序員的基本修養(yǎng),注意 NPE 產(chǎn)生的場(chǎng)景:
1)返回類(lèi)型為基本數(shù)據(jù)類(lèi)型, return 包裝數(shù)據(jù)類(lèi)型的對(duì)象時(shí),自動(dòng)拆箱有可能產(chǎn)生 NPE。
反例:public int f() { return Integer 對(duì)象}, 如果為 null,自動(dòng)解箱拋 NPE。
2) 數(shù)據(jù)庫(kù)的查詢(xún)結(jié)果可能為 null。
3) 集合里的元素即使 isNotEmpty,取出的數(shù)據(jù)元素也可能為 null。
4) 遠(yuǎn)程調(diào)用返回對(duì)象時(shí),一律要求進(jìn)行空指針判斷,防止 NPE。
5) 對(duì)于 Session 中獲取的數(shù)據(jù),建議 NPE 檢查,避免空指針。
6) 級(jí)聯(lián)調(diào)用 obj.getA().getB().getC(); 一連串調(diào)用,易產(chǎn)生 NPE。
正例: 使用 JDK8 的 Optional 類(lèi)來(lái)防止 NPE 問(wèn)題。
11、【推薦】 定義時(shí)區(qū)分 unchecked / checked 異常,避免直接拋出 new RuntimeException(),更不允許拋出 Exception 或者 Throwable,應(yīng)使用有業(yè)務(wù)含義的自定義異常。
推薦業(yè)界已定義過(guò)的自定義異常,如:DAOException / ServiceException 等。
12、【參考】 對(duì)于公司外的 http/api 開(kāi)放接口必須使用“錯(cuò)誤碼”; 而應(yīng)用內(nèi)部推薦異常拋出;跨應(yīng)用間 RPC 調(diào)用優(yōu)先考慮使用 Result 方式,封裝 isSuccess()方法、 “錯(cuò)誤碼”、 “錯(cuò)誤簡(jiǎn)短信息”。
說(shuō)明: 關(guān)于 RPC 方法返回方式使用 Result 方式的理由:
1) 使用拋異常返回方式,調(diào)用方如果沒(méi)有捕獲到就會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤。
2) 如果不加棧信息,只是 new 自定義異常,加入自己的理解的 error message,對(duì)于調(diào)用端解決問(wèn)題的幫助不會(huì)太多。如果加了棧信息,在頻繁調(diào)用出錯(cuò)的情況下,數(shù)據(jù)序列化和傳輸?shù)男阅軗p耗也是問(wèn)題。
13、【參考】 避免出現(xiàn)重復(fù)的代碼(Don’t Repeat Yourself) ,即 DRY 原則。
說(shuō)明: 隨意復(fù)制和粘貼代碼,必然會(huì)導(dǎo)致代碼的重復(fù),在以后需要修改時(shí),需要修改所有的副本,容易遺漏。必要時(shí)抽取共性方法,或者抽象公共類(lèi),甚至是組件化。
正例: 一個(gè)類(lèi)中有多個(gè) public 方法,都需要進(jìn)行數(shù)行相同的參數(shù)校驗(yàn)操作,這個(gè)時(shí)候請(qǐng)抽?。簆rivate boolean checkParam(DTO dto) {…}
常見(jiàn)面試題
1. Error 和 Exception 區(qū)別是什么?
Error 類(lèi)型的錯(cuò)誤通常為虛擬機(jī)相關(guān)錯(cuò)誤,如系統(tǒng)崩潰,內(nèi)存不足,堆棧溢出等,編譯器不會(huì)對(duì)這類(lèi)錯(cuò)誤進(jìn)行檢測(cè),JAVA 應(yīng)用程序也不應(yīng)對(duì)這類(lèi)錯(cuò)誤進(jìn)行捕獲,一旦這類(lèi)錯(cuò)誤發(fā)生,通常應(yīng)用程序會(huì)被終止,僅靠應(yīng)用程序本身無(wú)法恢復(fù);
Exception 類(lèi)的錯(cuò)誤是可以在應(yīng)用程序中進(jìn)行捕獲并處理的,通常遇到這種錯(cuò)誤,應(yīng)對(duì)其進(jìn)行處理,使應(yīng)用程序可以繼續(xù)正常運(yùn)行。
2. 運(yùn)行時(shí)異常和一般異常區(qū)別是什么?
編譯器不會(huì)對(duì)運(yùn)行時(shí)異常進(jìn)行檢測(cè),沒(méi)有 try-catch,方法簽名中也沒(méi)有 throws 關(guān)鍵字聲明,編譯依然可以通過(guò)。如果出現(xiàn)了 RuntimeException, 那一定是程序員的錯(cuò)誤。
一般一場(chǎng)如果沒(méi)有 try-catch,且方法簽名中也沒(méi)有用 throws 關(guān)鍵字聲明可能拋出的異常,則編譯無(wú)法通過(guò)。這類(lèi)異常通常為應(yīng)用環(huán)境中的錯(cuò)誤,即外部錯(cuò)誤,非應(yīng)用程序本身錯(cuò)誤,如文件找不到等。
3.NoClassDefFoundError 和 ClassNotFoundException 區(qū)別?
NoClassDefFoundError 是一個(gè) Error 類(lèi)型的異常,是由 JVM 引起的,不應(yīng)該嘗試捕獲這個(gè)異常。
引起該異常的原因是 JVM 或 ClassLoader 嘗試加載某類(lèi)時(shí)在內(nèi)存中找不到該類(lèi)的定義,該動(dòng)作發(fā)生在運(yùn)行期間,即編譯時(shí)該類(lèi)存在,但是在運(yùn)行時(shí)卻找不到了,可能是變異后被刪除了等原因?qū)е拢?/p>
ClassNotFoundException 是一個(gè)受查異常,需要顯式地使用 try-catch 對(duì)其進(jìn)行捕獲和處理,或在方法簽名中用 throws 關(guān)鍵字進(jìn)行聲明。當(dāng)使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 動(dòng)態(tài)加載類(lèi)到內(nèi)存的時(shí)候,通過(guò)傳入的類(lèi)路徑參數(shù)沒(méi)有找到該類(lèi),就會(huì)拋出該異常;另一種拋出該異常的可能原因是某個(gè)類(lèi)已經(jīng)由一個(gè)類(lèi)加載器加載至內(nèi)存中,另一個(gè)加載器又嘗試去加載它。
4. JVM 是如何處理異常的?
在一個(gè)方法中如果發(fā)生異常,這個(gè)方法會(huì)創(chuàng)建一個(gè)一場(chǎng)對(duì)象,并轉(zhuǎn)交給 JVM,該異常對(duì)象包含異常名稱(chēng),異常描述以及異常發(fā)生時(shí)應(yīng)用程序的狀態(tài)。創(chuàng)建異常對(duì)象并轉(zhuǎn)交給 JVM 的過(guò)程稱(chēng)為拋出異常。可能有一系列的方法調(diào)用,最終才進(jìn)入拋出異常的方法,這一系列方法調(diào)用的有序列表叫做調(diào)用棧。
JVM 會(huì)順著調(diào)用棧去查找看是否有可以處理異常的代碼,如果有,則調(diào)用異常處理代碼。當(dāng) JVM 發(fā)現(xiàn)可以處理異常的代碼時(shí),會(huì)把發(fā)生的異常傳遞給它。如果 JVM 沒(méi)有找到可以處理該異常的代碼塊,JVM 就會(huì)將該異常轉(zhuǎn)交給默認(rèn)的異常處理器(默認(rèn)處理器為 JVM 的一部分),默認(rèn)異常處理器打印出異常信息并終止應(yīng)用程序。
5. throw 和 throws 的區(qū)別是什么?
throw 關(guān)鍵字用來(lái)拋出方法或代碼塊中的異常,受查異常和非受查異常都可以被拋出。
throws 關(guān)鍵字用在方法簽名處,用來(lái)標(biāo)識(shí)該方法可能拋出的異常列表。一個(gè)方法用 throws 標(biāo)識(shí)了可能拋出的異常列表,調(diào)用該方法的方法中必須包含可處理異常的代碼,否則也要在方法簽名中用 throws 關(guān)鍵字聲明相應(yīng)的異常。
6. 常見(jiàn)的 RuntimeException 有哪些?
- ClassCastException(類(lèi)轉(zhuǎn)換異常)
- IndexOutOfBoundsException(數(shù)組越界)
- NullPointerException(空指針)
- ArrayStoreException(數(shù)據(jù)存儲(chǔ)異常,操作數(shù)組時(shí)類(lèi)型不一致)
- 還有IO操作的BufferOverflowException異常