一文讀懂 JAVA 異常處理

JAVA 異常類(lèi)型結(jié)構(gòu)

Error 和 Exeption

受查異常和非受查異常

異常的拋出與捕獲

直接拋出異常

封裝異常并拋出

捕獲異常

自定義異常

try-catch-finally

try-with-resource

阿里巴巴異常處理規(guī)約

常見(jiàn)面試題

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)。

privatestaticvoidreadFile(String filePath)throwsIOException{? ? File file =newFile(filePath);? ? String result;? ? BufferedReader reader =newBufferedReader(newFileReader(file));while((result = reader.readLine())!=null) {? ? ? ? System.out.println(result);? ? }? ? reader.close();}

privatestaticvoidreadFile(String filePath){? ? File file =newFile(filePath);? ? String result;? ? BufferedReader reader;try{? ? ? ? reader =newBufferedReader(newFileReader(file));while((result = reader.readLine())!=null) {? ? ? ? ? ? System.out.println(result);? ? ? ? }? ? ? ? reader.close();? ? }catch(IOException e) {? ? ? ? e.printStackTrace();? ? }}

非受查異常

非受查異常不會(huì)在編譯時(shí)被檢測(cè)。JAVA 中 Error 和 RuntimeException 類(lèi)的子類(lèi)屬于非受查異常,除此之外繼承自 Exception 的類(lèi)型為受查異常。

異常的拋出與捕獲

直接拋出異常

通常,應(yīng)該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續(xù)傳遞下去。傳遞異??梢栽诜椒ê灻幨褂?throws?關(guān)鍵字聲明可能會(huì)拋出的異常。

privatestaticvoidreadFile(String filePath)throwsIOException{? ? File file =newFile(filePath);? ? String result;? ? BufferedReader reader =newBufferedReader(newFileReader(file));while((result = reader.readLine())!=null) {? ? ? ? System.out.println(result);? ? }? ? reader.close();}

封裝異常再拋出

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

privatestaticvoidreadFile(String filePath)throwsMyException{try{// code}catch(IOException e) {? ? ? ? MyException ex =newMyException("read file failed.");? ? ? ? ex.initCause(e);throwex;? ? }}

捕獲異常

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

privatestaticvoidreadFile(String filePath){try{// code}catch(FileNotFoundException e) {// handle FileNotFoundException}catch(IOException e){// handle IOException}}

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

privatestaticvoidreadFile(String filePath){try{// code}catch(FileNotFoundException | UnknownHostException e) {// handle FileNotFoundException or UnknownHostException}catch(IOException e){// handle IOException}}

自定義異常

習(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í)很有用)

publicclassMyExceptionextendsException{publicMyException(){ }publicMyException(String msg){super(msg);? ? }// ...}??

try-catch-finally

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

privatestaticvoidreadFile(String filePath)throwsMyException{? ? File file =newFile(filePath);? ? String result;? ? BufferedReader reader =null;try{? ? ? ? reader =newBufferedReader(newFileReader(file));while((result = reader.readLine())!=null) {? ? ? ? ? ? System.out.println(result);? ? ? ? }? ? }catch(IOException e) {? ? ? ? System.out.println("readFile method catch block.");? ? ? ? MyException ex =newMyException("read file failed.");? ? ? ? ex.initCause(e);throwex;? ? }finally{? ? ? ? System.out.println("readFile method finally block.");if(null!= reader) {try{? ? ? ? ? ? ? ? reader.close();? ? ? ? ? ? }catch(IOException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }? ? ? ? }? ? }}

調(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 子句修改如下

catch(IOException e) {? ? System.out.println("readFile method catch block.");return;}

調(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)。

privatestaticvoidtryWithResourceTest(){try(Scanner scanner =newScanner(newFileInputStream("c:/abc"),"UTF-8")){// code}catch(IOException e){// handle exception}}

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ī)約

【強(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) {…}

【強(qiáng)制】?異常不要用來(lái)做流程控制,條件控制。

說(shuō)明: 異常設(shè)計(jì)的初衷是解決程序運(yùn)行中的各種意外情況,且異常的處理效率比條件判斷方式

要低很多。

【強(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ù)。

【強(qiáng)制】?捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請(qǐng)

將該異常拋給它的調(diào)用者。最外層的業(yè)務(wù)使用者,必須處理異常,將其轉(zhuǎn)化為用戶(hù)可以理解的

內(nèi)容。

【強(qiáng)制】?有 try 塊放到了事務(wù)代碼中, catch 異常后,如果需要回滾事務(wù),一定要注意手動(dòng)回

滾事務(wù)。

【強(qiáng)制】?finally 塊必須對(duì)資源對(duì)象、流對(duì)象進(jìn)行關(guān)閉,有異常也要做 try-catch。

說(shuō)明: 如果 JDK7 及以上,可以使用 try-with-resources 方式。

【強(qiáng)制】?不要在 finally 塊中使用 return。

說(shuō)明: finally 塊中的 return 返回后方法結(jié)束執(zhí)行,不會(huì)再執(zhí)行 try 塊中的 return 語(yǔ)句。

【強(qiáng)制】?捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類(lèi)。

說(shuō)明: 如果預(yù)期對(duì)方拋的是繡球,實(shí)際接到的是鉛球,就會(huì)產(chǎn)生意外情況。

【推薦】?方法的返回值可以為 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 的情況。

【推薦】?防止 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)題。

【推薦】?定義時(shí)區(qū)分 unchecked / checked 異常,避免直接拋出 new RuntimeException(),

更不允許拋出 Exception 或者 Throwable,應(yīng)使用有業(yè)務(wù)含義的自定義異常。推薦業(yè)界已定義

過(guò)的自定義異常,如: DAOException / ServiceException 等。

【參考】?對(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ù)序列化和傳輸

的性能損耗也是問(wèn)題。

【參考】?避免出現(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)抽?。?/p>

private boolean checkParam(DTO dto) {...}

常見(jiàn)面試題

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)行。

運(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ò)誤,如文件找不到等。

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è)加載器又嘗試去加載它。

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)用程序。

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)的異常。??

常見(jiàn)的 RuntimeException 有哪些?

ClassCastException(類(lèi)轉(zhuǎn)換異常)

IndexOutOfBoundsException(數(shù)組越界)

NullPointerException(空指針)

ArrayStoreException(數(shù)據(jù)存儲(chǔ)異常,操作數(shù)組時(shí)類(lèi)型不一致)

還有IO操作的BufferOverflowException異常 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 歡迎工作一到五年的Java工程師朋友們加入Java群:?741514154

群內(nèi)提供免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個(gè)知識(shí)點(diǎn)的架構(gòu)資料)合理利用自己每一分每一秒的時(shí)間來(lái)學(xué)習(xí)提升自己,不要再用"沒(méi)有時(shí)間“來(lái)掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來(lái)的自己一個(gè)交代!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容