夯实Java基础系列10:深入理解Java中的异常体系
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內(nèi)容請到我的倉庫里查看
https://github.com/h2pl/Java-Tutorial
喜歡的話麻煩點下Star、Fork、Watch三連哈,感謝你的支持。
文章首發(fā)于我的個人博客:
www.how2playlife.com
本文是微信公眾號【Java技術(shù)江湖】的《夯實Java基礎(chǔ)系列博文》其中一篇,本文部分內(nèi)容來源于網(wǎng)絡(luò),為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章,如有侵權(quán),請聯(lián)系作者。
該系列博文會告訴你如何從入門到進階,一步步地學(xué)習(xí)Java基礎(chǔ)知識,并上手進行實戰(zhàn),接著了解每個Java知識點背后的實現(xiàn)原理,更完整地了解整個Java技術(shù)體系,形成自己的知識框架。為了更好地總結(jié)和檢驗?zāi)愕膶W(xué)習(xí)成果,本系列文章也會提供每個知識點對應(yīng)的面試題以及參考答案。
文章目錄
- 為什么要使用異常
- 異常基本定義
- 異常體系
- 初識異常
- 異常和錯誤
- 異常的處理方式
- "不負責(zé)任"的throws
- 糾結(jié)的finally
- throw : JRE也使用的關(guān)鍵字
- 異常調(diào)用鏈
- 自定義異常
- 異常的注意事項
- 當finally遇上return
- JAVA異常常見面試題
- 參考文章
- 微信公眾號
- 個人公眾號:程序員黃小斜
- 技術(shù)公眾號:Java技術(shù)江湖
如果對本系列文章有什么建議,或者是有什么疑問的話,也可以關(guān)注公眾號【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂。
為什么要使用異常
首先我們可以明確一點就是異常的處理機制可以確保我們程序的健壯性,提高系統(tǒng)可用率。雖然我們不是特別喜歡看到它,但是我們不能不承認它的地位,作用。
在沒有異常機制的時候我們是這樣處理的:通過函數(shù)的返回值來判斷是否發(fā)生了異常(這個返回值通常是已經(jīng)約定好了的),調(diào)用該函數(shù)的程序負責(zé)檢查并且分析返回值。雖然可以解決異常問題,但是這樣做存在幾個缺陷:
1、 容易混淆。如果約定返回值為-11111時表示出現(xiàn)異常,那么當程序最后的計算結(jié)果真的為-1111呢?
2、 代碼可讀性差。將異常處理代碼和程序代碼混淆在一起將會降低代碼的可讀性。
3、 由調(diào)用函數(shù)來分析異常,這要求程序員對庫函數(shù)有很深的了解。
在OO中提供的異常處理機制是提供代碼健壯的強有力的方式。使用異常機制它能夠降低錯誤處理代碼的復(fù)雜度,如果不使用異常,那么就必須檢查特定的錯誤,并在程序中的許多地方去處理它。
而如果使用異常,那就不必在方法調(diào)用處進行檢查,因為異常機制將保證能夠捕獲這個錯誤,并且,只需在一個地方處理錯誤,即所謂的異常處理程序中。
這種方式不僅節(jié)約代碼,而且把“概述在正常執(zhí)行過程中做什么事”的代碼和“出了問題怎么辦”的代碼相分離。總之,與以前的錯誤處理方法相比,異常機制使代碼的閱讀、編寫和調(diào)試工作更加井井有條。(摘自《Think in java 》)。
該部分內(nèi)容選自http://www.cnblogs.com/chenssy/p/3438130.html
異常基本定義
在《Think in java》中是這樣定義異常的:異常情形是指阻止當前方法或者作用域繼續(xù)執(zhí)行的問題。在這里一定要明確一點:異常代碼某種程度的錯誤,盡管Java有異常處理機制,但是我們不能以“正常”的眼光來看待異常,異常處理機制的原因就是告訴你:這里可能會或者已經(jīng)產(chǎn)生了錯誤,您的程序出現(xiàn)了不正常的情況,可能會導(dǎo)致程序失敗!
那么什么時候才會出現(xiàn)異常呢?只有在你當前的環(huán)境下程序無法正常運行下去,也就是說程序已經(jīng)無法來正確解決問題了,這時它所就會從當前環(huán)境中跳出,并拋出異常。拋出異常后,它首先會做幾件事。
首先,它會使用new創(chuàng)建一個異常對象,然后在產(chǎn)生異常的位置終止程序,并且從當前環(huán)境中彈出對異常對象的引用,這時。異常處理機制就會接管程序,并開始尋找一個恰當?shù)牡胤絹砝^續(xù)執(zhí)行程序,這個恰當?shù)牡胤骄褪钱惓L幚沓绦颉?/p>
總的來說異常處理機制就是當程序發(fā)生異常時,它強制終止程序運行,記錄異常信息并將這些信息反饋給我們,由我們來確定是否處理異常。
異常體系
[外鏈圖片轉(zhuǎn)存失敗(img-KNxcBTK0-1569073569353)(https://images0.cnblogs.com/blog/381060/201311/22185952-834d92bc2bfe498f9a33414cc7a2c8a4.png)]
從上面這幅圖可以看出,Throwable是java語言中所有錯誤和異常的超類(萬物即可拋)。它有兩個子類:Error、Exception。
Java標準庫內(nèi)建了一些通用的異常,這些類以Throwable為頂層父類。
Throwable又派生出Error類和Exception類。
錯誤:Error類以及他的子類的實例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理,Error很少出現(xiàn)。因此,程序員應(yīng)該關(guān)注Exception為父類的分支下的各種異常類。
異常:Exception以及他的子類,代表程序運行時發(fā)送的各種不期望發(fā)生的事件。可以被Java異常處理機制使用,是異常處理的核心。
總體上我們根據(jù)Javac對異常的處理要求,將異常類分為2類。
非檢查異常(unckecked exception):Error 和 RuntimeException 以及他們的子類。javac在編譯時,不會提示和發(fā)現(xiàn)這樣的異常,不要求在程序處理這些異常。所以如果愿意,我們可以編寫代碼處理(使用try…catch…finally)這樣的異常,也可以不處理。
對于這些異常,我們應(yīng)該修正代碼,而不是去通過異常處理器處理 。這樣的異常發(fā)生的原因多半是代碼寫的有問題。如除0錯誤ArithmeticException,錯誤的強制類型轉(zhuǎn)換錯誤ClassCastException,數(shù)組索引越界ArrayIndexOutOfBoundsException,使用了空對象NullPointerException等等。
檢查異常(checked exception):除了Error 和 RuntimeException的其它異常。javac強制要求程序員為這樣的異常做預(yù)備處理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch語句捕獲它并處理,要么用throws子句聲明拋出它,否則編譯不會通過。
這樣的異常一般是由程序的運行環(huán)境導(dǎo)致的。因為程序可能被運行在各種未知的環(huán)境下,而程序員無法干預(yù)用戶如何使用他編寫的程序,于是程序員就應(yīng)該為這樣的異常時刻準備著。如SQLException , IOException,ClassNotFoundException 等。
需要明確的是:檢查和非檢查是對于javac來說的,這樣就很好理解和區(qū)分了。
這部分內(nèi)容摘自http://www.importnew.com/26613.html
初識異常
異常是在執(zhí)行某個函數(shù)時引發(fā)的,而函數(shù)又是層級調(diào)用,形成調(diào)用棧的,因為,只要一個函數(shù)發(fā)生了異常,那么他的所有的caller都會被異常影響。當這些被影響的函數(shù)以異常信息輸出時,就形成的了異常追蹤棧。
異常最先發(fā)生的地方,叫做異常拋出點。
public class 異常 {public static void main (String [] args ){System . out. println( "----歡迎使用命令行除法計算器----" ) ;CMDCalculate ();}public static void CMDCalculate (){Scanner scan = new Scanner ( System. in );int num1 = scan .nextInt () ;int num2 = scan .nextInt () ;int result = devide (num1 , num2 ) ;System . out. println( "result:" + result) ;scan .close () ;}public static int devide (int num1, int num2 ){return num1 / num2 ;}// ----歡迎使用命令行除法計算器---- // 1 // 0 // Exception in thread "main" java.lang.ArithmeticException: / by zero // at com.javase.異常.異常.devide(異常.java:24) // at com.javase.異常.異常.CMDCalculate(異常.java:19) // at com.javase.異常.異常.main(異常.java:12)?
// ----歡迎使用命令行除法計算器---- // r // Exception in thread "main" java.util.InputMismatchException // at java.util.Scanner.throwFor(Scanner.java:864) // at java.util.Scanner.next(Scanner.java:1485) // at java.util.Scanner.nextInt(Scanner.java:2117) // at java.util.Scanner.nextInt(Scanner.java:2076) // at com.javase.異常.異常.CMDCalculate(異常.java:17) // at com.javase.異常.異常.main(異常.java:12)[外鏈圖片轉(zhuǎn)存失敗(img-9rqUQJQj-1569073569354)(http://incdn1.b0.upaiyun.com/2017/09/0b3e4ca2f4cf8d7116c7ad354940601f.png)]
從上面的例子可以看出,當devide函數(shù)發(fā)生除0異常時,devide函數(shù)將拋出ArithmeticException異常,因此調(diào)用他的CMDCalculate函數(shù)也無法正常完成,因此也發(fā)送異常,而CMDCalculate的caller——main 因為CMDCalculate拋出異常,也發(fā)生了異常,這樣一直向調(diào)用棧的棧底回溯。
這種行為叫做異常的冒泡,異常的冒泡是為了在當前發(fā)生異常的函數(shù)或者這個函數(shù)的caller中找到最近的異常處理程序。由于這個例子中沒有使用任何異常處理機制,因此異常最終由main函數(shù)拋給JRE,導(dǎo)致程序終止。
上面的代碼不使用異常處理機制,也可以順利編譯,因為2個異常都是非檢查異常。但是下面的例子就必須使用異常處理機制,因為異常是檢查異常。
代碼中我選擇使用throws聲明異常,讓函數(shù)的調(diào)用者去處理可能發(fā)生的異常。但是為什么只throws了IOException呢?因為FileNotFoundException是IOException的子類,在處理范圍內(nèi)。
異常和錯誤
下面看一個例子
//錯誤即error一般指jvm無法處理的錯誤 //異常是Java定義的用于簡化錯誤處理流程和定位錯誤的一種工具。 public class 錯誤和錯誤 {Error error = new Error();public static void main(String[] args) {throw new Error();}//下面這四個異常或者錯誤有著不同的處理方法public void error1 (){//編譯期要求必須處理,因為這個異常是最頂層異常,包括了檢查異常,必須要處理try {throw new Throwable();} catch (Throwable throwable) {throwable.printStackTrace();}}//Exception也必須處理。否則報錯,因為檢查異常都繼承自exception,所以默認需要捕捉。public void error2 (){try {throw new Exception();} catch (Exception e) {e.printStackTrace();}}//error可以不處理,編譯不報錯,原因是虛擬機根本無法處理,所以啥都不用做public void error3 (){throw new Error();}//runtimeexception眾所周知編譯不會報錯public void error4 (){throw new RuntimeException();} // Exception in thread "main" java.lang.Error // at com.javase.異常.錯誤.main(錯誤.java:11)}異常的處理方式
在編寫代碼處理異常時,對于檢查異常,有2種不同的處理方式:
使用try…catch…finally語句塊處理它。
或者,在函數(shù)簽名中使用throws 聲明交給函數(shù)調(diào)用者caller去解決。
下面看幾個具體的例子,包括error,exception和throwable
上面的例子是運行時異常,不需要顯示捕獲。
下面這個例子是可檢查異常需,要顯示捕獲或者拋出。
一般情況下的處理方式 try catch finally
public class 異常處理方式 {@Test public void main() {try{//try塊中放可能發(fā)生異常的代碼。InputStream inputStream = new FileInputStream("a.txt");//如果執(zhí)行完try且不發(fā)生異常,則接著去執(zhí)行finally塊和finally后面的代碼(如果有的話)。int i = 1/0;//如果發(fā)生異常,則嘗試去匹配catch塊。throw new SQLException();//使用1.8jdk同時捕獲多個異常,runtimeexception也可以捕獲。只是捕獲后虛擬機也無法處理,所以不建議捕獲。}catch(SQLException | IOException | ArrayIndexOutOfBoundsException exception){System.out.println(exception.getMessage());//每一個catch塊用于捕獲并處理一個特定的異常,或者這異常類型的子類。Java7中可以將多個異常聲明在一個catch中。//catch后面的括號定義了異常類型和異常參數(shù)。如果異常與之匹配且是最先匹配到的,則虛擬機將使用這個catch塊來處理異常。//在catch塊中可以使用這個塊的異常參數(shù)來獲取異常的相關(guān)信息。異常參數(shù)是這個catch塊中的局部變量,其它塊不能訪問。//如果當前try塊中發(fā)生的異常在后續(xù)的所有catch中都沒捕獲到,則先去執(zhí)行finally,然后到這個函數(shù)的外部caller中去匹配異常處理器。//如果try中沒有發(fā)生異常,則所有的catch塊將被忽略。}catch(Exception exception){System.out.println(exception.getMessage());//...}finally{//finally塊通常是可選的。//無論異常是否發(fā)生,異常是否匹配被處理,finally都會執(zhí)行。//finally主要做一些清理工作,如流的關(guān)閉,數(shù)據(jù)庫連接的關(guān)閉等。}一個try至少要跟一個catch或者finally
try {int i = 1;}finally {//一個try至少要有一個catch塊,否則, 至少要有1個finally塊。但是finally不是用來處理異常的,finally不會捕獲異常。} }異常出現(xiàn)時該方法后面的代碼不會運行,即使異常已經(jīng)被捕獲。這里舉出一個奇特的例子,在catch里再次使用try catch finally
@Test public void test() {try {throwE();System.out.println("我前面拋出異常了");System.out.println("我不會執(zhí)行了");} catch (StringIndexOutOfBoundsException e) {System.out.println(e.getCause());}catch (Exception ex) {//在catch塊中仍然可以使用try catch finallytry {throw new Exception();}catch (Exception ee) {}finally {System.out.println("我所在的catch塊沒有執(zhí)行,我也不會執(zhí)行的");}} } //在方法聲明中拋出的異常必須由調(diào)用方法處理或者繼續(xù)往上拋, // 當拋到j(luò)re時由于無法處理終止程序 public void throwE (){ // Socket socket = new Socket("127.0.0.1", 80);//手動拋出異常時,不會報錯,但是調(diào)用該方法的方法需要處理這個異常,否則會出錯。 // java.lang.StringIndexOutOfBoundsException // at com.javase.異常.異常處理方式.throwE(異常處理方式.java:75) // at com.javase.異常.異常處理方式.test(異常處理方式.java:62)throw new StringIndexOutOfBoundsException();}其實有的語言在遇到異常后仍然可以繼續(xù)運行
有的編程語言當異常被處理后,控制流會恢復(fù)到異常拋出點接著執(zhí)行,這種策略叫做:resumption model of exception handling(恢復(fù)式異常處理模式 )
而Java則是讓執(zhí)行流恢復(fù)到處理了異常的catch塊后接著執(zhí)行,這種策略叫做:termination model of exception handling(終結(jié)式異常處理模式)
"不負責(zé)任"的throws
throws是另一種處理異常的方式,它不同于try…catch…finally,throws僅僅是將函數(shù)中可能出現(xiàn)的異常向調(diào)用者聲明,而自己則不具體處理。
采取這種異常處理的原因可能是:方法本身不知道如何處理這樣的異常,或者說讓調(diào)用者處理更好,調(diào)用者需要為可能發(fā)生的異常負責(zé)。
public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN { //foo內(nèi)部可以拋出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 類的異常,或者他們的子類的異常對象。 }糾結(jié)的finally
finally塊不管異常是否發(fā)生,只要對應(yīng)的try執(zhí)行了,則它一定也執(zhí)行。只有一種方法讓finally塊不執(zhí)行:System.exit()。因此finally塊通常用來做資源釋放操作:關(guān)閉文件,關(guān)閉數(shù)據(jù)庫連接等等。
良好的編程習(xí)慣是:在try塊中打開資源,在finally塊中清理釋放這些資源。
需要注意的地方:
1、finally塊沒有處理異常的能力。處理異常的只能是catch塊。
2、在同一try…catch…finally塊中 ,如果try中拋出異常,且有匹配的catch塊,則先執(zhí)行catch塊,再執(zhí)行finally塊。如果沒有catch塊匹配,則先執(zhí)行finally,然后去外面的調(diào)用者中尋找合適的catch塊。
3、在同一try…catch…finally塊中 ,try發(fā)生異常,且匹配的catch塊中處理異常時也拋出異常,那么后面的finally也會執(zhí)行:首先執(zhí)行finally塊,然后去外圍調(diào)用者中尋找合適的catch塊。
public class finally使用 {public static void main(String[] args) {try {throw new IllegalAccessException();}catch (IllegalAccessException e) {// throw new Throwable();//此時如果再拋異常,finally無法執(zhí)行,只能報錯。//finally無論何時都會執(zhí)行//除非我顯示調(diào)用。此時finally才不會執(zhí)行System.exit(0);}finally {System.out.println("算你狠");}} }throw : JRE也使用的關(guān)鍵字
throw exceptionObject
程序員也可以通過throw語句手動顯式的拋出一個異常。throw語句的后面必須是一個異常對象。
throw 語句必須寫在函數(shù)中,執(zhí)行throw 語句的地方就是一個異常拋出點,它和由JRE自動形成的異常拋出點沒有任何差別。
public void save(User user) {if(user == null) throw new IllegalArgumentException("User對象為空");//......}后面開始的大部分內(nèi)容都摘自http://www.cnblogs.com/lulipro/p/7504267.html
該文章寫的十分細致到位,令人欽佩,是我目前為之看到關(guān)于異常最詳盡的文章,可以說是站在巨人的肩膀上了。
異常調(diào)用鏈
異常的鏈化
在一些大型的,模塊化的軟件開發(fā)中,一旦一個地方發(fā)生異常,則如骨牌效應(yīng)一樣,將導(dǎo)致一連串的異常。假設(shè)B模塊完成自己的邏輯需要調(diào)用A模塊的方法,如果A模塊發(fā)生異常,則B也將不能完成而發(fā)生異常。
但是B在拋出異常時,會將A的異常信息掩蓋掉,這將使得異常的根源信息丟失。異常的鏈化可以將多個模塊的異常串聯(lián)起來,使得異常信息不會丟失。
異常鏈化:以一個異常對象為參數(shù)構(gòu)造新的異常對象。新的異對象將包含先前異常的信息。這項技術(shù)主要是異常類的一個帶Throwable參數(shù)的函數(shù)來實現(xiàn)的。這個當做參數(shù)的異常,我們叫他根源異常(cause)。
查看Throwable類源碼,可以發(fā)現(xiàn)里面有一個Throwable字段cause,就是它保存了構(gòu)造時傳遞的根源異常參數(shù)。這種設(shè)計和鏈表的結(jié)點類設(shè)計如出一轍,因此形成鏈也是自然的了。
public class Throwable implements Serializable {private Throwable cause = this;public Throwable(String message, Throwable cause) {fillInStackTrace();detailMessage = message;this.cause = cause;}public Throwable(Throwable cause) {fillInStackTrace();detailMessage = (cause==null ? null : cause.toString());this.cause = cause;}//........ }下面看一個比較實在的異常鏈例子哈
public class 異常鏈 {@Testpublic void test() {C();}public void A () throws Exception {try {int i = 1;i = i / 0;//當我注釋掉這行代碼并使用B方法拋出一個error時,運行結(jié)果如下 // 四月 27, 2018 10:12:30 下午 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines // 信息: Discovered TestEngines with IDs: [junit-jupiter] // java.lang.Error: B也犯了個錯誤 // at com.javase.異常.異常鏈.B(異常鏈.java:33) // at com.javase.異常.異常鏈.C(異常鏈.java:38) // at com.javase.異常.異常鏈.test(異常鏈.java:13) // at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) // Caused by: java.lang.Error // at com.javase.異常.異常鏈.B(異常鏈.java:29)}catch (ArithmeticException e) {//這里通過throwable類的構(gòu)造方法將最底層的異常重新包裝并拋出,此時注入了A方法的信息。最后打印棧信息時可以看到caused byA方法的異常。//如果直接拋出,棧信息打印結(jié)果只能看到上層方法的錯誤信息,不能看到其實是A發(fā)生了錯誤。//所以需要包裝并拋出throw new Exception("A方法計算錯誤", e);}}public void B () throws Exception,Error {try {//接收到A的異常,A();throw new Error();}catch (Exception e) {throw e;}catch (Error error) {throw new Error("B也犯了個錯誤", error);}}public void C () {try {B();}catch (Exception | Error e) {e.printStackTrace();}}//最后結(jié)果 // java.lang.Exception: A方法計算錯誤 // at com.javase.異常.異常鏈.A(異常鏈.java:18) // at com.javase.異常.異常鏈.B(異常鏈.java:24) // at com.javase.異常.異常鏈.C(異常鏈.java:31) // at com.javase.異常.異常鏈.test(異常鏈.java:11) // 省略 // Caused by: java.lang.ArithmeticException: / by zero // at com.javase.異常.異常鏈.A(異常鏈.java:16) // ... 31 more }自定義異常
如果要自定義異常類,則擴展Exception類即可,因此這樣的自定義異常都屬于檢查異常(checked exception)。如果要自定義非檢查異常,則擴展自RuntimeException。
按照國際慣例,自定義的異常應(yīng)該總是包含如下的構(gòu)造函數(shù):
一個無參構(gòu)造函數(shù)
一個帶有String參數(shù)的構(gòu)造函數(shù),并傳遞給父類的構(gòu)造函數(shù)。
一個帶有String參數(shù)和Throwable參數(shù),并都傳遞給父類構(gòu)造函數(shù)
一個帶有Throwable 參數(shù)的構(gòu)造函數(shù),并傳遞給父類的構(gòu)造函數(shù)。
下面是IOException類的完整源代碼,可以借鑒。
異常的注意事項
異常的注意事項
當子類重寫父類的帶有 throws聲明的函數(shù)時,其throws聲明的異常必須在父類異常的可控范圍內(nèi)——用于處理父類的throws方法的異常處理器,必須也適用于子類的這個帶throws方法 。這是為了支持多態(tài)。
例如,父類方法throws 的是2個異常,子類就不能throws 3個及以上的異常。父類throws IOException,子類就必須throws IOException或者IOException的子類。
至于為什么?我想,也許下面的例子可以說明。
class Father {public void start() throws IOException{throw new IOException();} }class Son extends Father {public void start() throws Exception{throw new SQLException();} }/**********************假設(shè)上面的代碼是允許的(實質(zhì)是錯誤的)***********************/
class Test {public static void main(String[] args){Father[] objs = new Father[2];objs[0] = new Father();objs[1] = new Son();for(Father obj:objs){//因為Son類拋出的實質(zhì)是SQLException,而IOException無法處理它。//那么這里的try。。catch就不能處理Son中的異常。//多態(tài)就不能實現(xiàn)了。try {obj.start();}catch(IOException){//處理IOException}}} }Java的異常執(zhí)行流程是線程獨立的,線程之間沒有影響
Java程序可以是多線程的。每一個線程都是一個獨立的執(zhí)行流,獨立的函數(shù)調(diào)用棧。如果程序只有一個線程,那么沒有被任何代碼處理的異常 會導(dǎo)致程序終止。如果是多線程的,那么沒有被任何代碼處理的異常僅僅會導(dǎo)致異常所在的線程結(jié)束。
也就是說,Java中的異常是線程獨立的,線程的問題應(yīng)該由線程自己來解決,而不要委托到外部,也不會直接影響到其它線程的執(zhí)行。
下面看一個例子
public class 多線程的異常 {@Testpublic void test() {go();}public void go () {ExecutorService executorService = Executors.newFixedThreadPool(3);for (int i = 0;i <= 2;i ++) {int finalI = i;try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}executorService.execute(new Runnable() {@Override//每個線程拋出異常時并不會影響其他線程的繼續(xù)執(zhí)行public void run() {try {System.out.println("start thread" + finalI);throw new Exception();}catch (Exception e) {System.out.println("thread" + finalI + " go wrong");}}});} // 結(jié)果: // start thread0 // thread0 go wrong // start thread1 // thread1 go wrong // start thread2 // thread2 go wrong} }當finally遇上return
首先一個不容易理解的事實:
在 try塊中即便有return,break,continue等改變執(zhí)行流的語句,finally也會執(zhí)行。
public static void main(String[] args) {int re = bar();System.out.println(re); } private static int bar() {try{return 5;} finally{System.out.println("finally");} } /*輸出: finally */很多人面對這個問題時,總是在歸納執(zhí)行的順序和規(guī)律,不過我覺得還是很難理解。我自己總結(jié)了一個方法。用如下GIF圖說明。
[外鏈圖片轉(zhuǎn)存失敗(img-SceF4t85-1569073569354)(http://incdn1.b0.upaiyun.com/2017/09/0471c2805ebd5a463211ced478eaf7f8.gif)]
也就是說:try…catch…finally中的return 只要能執(zhí)行,就都執(zhí)行了,他們共同向同一個內(nèi)存地址(假設(shè)地址是0×80)寫入返回值,后執(zhí)行的將覆蓋先執(zhí)行的數(shù)據(jù),而真正被調(diào)用者取的返回值就是最后一次寫入的。那么,按照這個思想,下面的這個例子也就不難理解了。
finally中的return 會覆蓋 try 或者catch中的返回值。
public static void main(String[] args){int result;result = foo();System.out.println(result); /2result = bar();System.out.println(result); /2}@SuppressWarnings("finally")public static int foo(){trz{int a = 5 / 0;} catch (Exception e){return 1;} finally{return 2;}}@SuppressWarnings("finally")public static int bar(){try {return 1;}finally {return 2;}}finally中的return會抑制(消滅)前面try或者catch塊中的異常
class TestException {public static void main(String[] args){int result;try{result = foo();System.out.println(result); //輸出100} catch (Exception e){System.out.println(e.getMessage()); //沒有捕獲到異常}try{result = bar();System.out.println(result); //輸出100} catch (Exception e){System.out.println(e.getMessage()); //沒有捕獲到異常}}//catch中的異常被抑制@SuppressWarnings("finally")public static int foo() throws Exception{try {int a = 5/0;return 1;}catch(ArithmeticException amExp) {throw new Exception("我將被忽略,因為下面的finally中使用了return");}finally {return 100;}}//try中的異常被抑制@SuppressWarnings("finally")public static int bar() throws Exception{try {int a = 5/0;return 1;}finally {return 100;}} }finally中的異常會覆蓋(消滅)前面try或者catch中的異常
class TestException {public static void main(String[] args){int result;try{result = foo();} catch (Exception e){System.out.println(e.getMessage()); //輸出:我是finaly中的Exception}try{result = bar();} catch (Exception e){System.out.println(e.getMessage()); //輸出:我是finaly中的Exception}}//catch中的異常被抑制@SuppressWarnings("finally")public static int foo() throws Exception{try {int a = 5/0;return 1;}catch(ArithmeticException amExp) {throw new Exception("我將被忽略,因為下面的finally中拋出了新的異常");}finally {throw new Exception("我是finaly中的Exception");}}//try中的異常被抑制@SuppressWarnings("finally")public static int bar() throws Exception{try {int a = 5/0;return 1;}finally {throw new Exception("我是finaly中的Exception");}} }上面的3個例子都異于常人的編碼思維,因此我建議:
不要在fianlly中使用return。
不要在finally中拋出異常。
減輕finally的任務(wù),不要在finally中做一些其它的事情,finally塊僅僅用來釋放資源是最合適的。
將盡量將所有的return寫在函數(shù)的最后面,而不是try … catch … finally中。
JAVA異常常見面試題
下面是我個人總結(jié)的在Java和J2EE開發(fā)者在面試中經(jīng)常被問到的有關(guān)Exception和Error的知識。在分享我的回答的時候,我也給這些問題作了快速修訂,并且提供源碼以便深入理解。我總結(jié)了各種難度的問題,適合新手碼農(nóng)和高級Java碼農(nóng)。如果你遇到了我列表中沒有的問題,并且這個問題非常好,請在下面評論中分享出來。你也可以在評論中分享你面試時答錯的情況。
1) Java中什么是Exception?
這個問題經(jīng)常在第一次問有關(guān)異常的時候或者是面試菜鳥的時候問。我從來沒見過面高級或者資深工程師的時候有人問這玩意,但是對于菜鳥,是很愿意問這個的。簡單來說,異常是Java傳達給你的系統(tǒng)和程序錯誤的方式。在java中,異常功能是通過實現(xiàn)比如Throwable,Exception,RuntimeException之類的類,然后還有一些處理異常時候的關(guān)鍵字,比如throw,throws,try,catch,finally之類的。 所有的異常都是通過Throwable衍生出來的。Throwable把錯誤進一步劃分為 java.lang.Exception
和 java.lang.Error. java.lang.Error 用來處理系統(tǒng)錯誤,例如java.lang.StackOverFlowError 之類的。然后 Exception用來處理程序錯誤,請求的資源不可用等等。
2) Java中的檢查型異常和非檢查型異常有什么區(qū)別?
這又是一個非常流行的Java異常面試題,會出現(xiàn)在各種層次的Java面試中。檢查型異常和非檢查型異常的主要區(qū)別在于其處理方式。檢查型異常需要使用try, catch和finally關(guān)鍵字在編譯期進行處理,否則會出現(xiàn)編譯器會報錯。對于非檢查型異常則不需要這樣做。Java中所有繼承自java.lang.Exception類的異常都是檢查型異常,所有繼承自RuntimeException的異常都被稱為非檢查型異常。
3) Java中的NullPointerException和ArrayIndexOutOfBoundException之間有什么相同之處?
在Java異常面試中這并不是一個很流行的問題,但會出現(xiàn)在不同層次的初學(xué)者面試中,用來測試應(yīng)聘者對檢查型異常和非檢查型異常的概念是否熟悉。順便說一下,該題的答案是,這兩個異常都是非檢查型異常,都繼承自RuntimeException。該問題可能會引出另一個問題,即Java和C的數(shù)組有什么不同之處,因為C里面的數(shù)組是沒有大小限制的,絕對不會拋出ArrayIndexOutOfBoundException。
4)在Java異常處理的過程中,你遵循的那些最好的實踐是什么?
這個問題在面試技術(shù)經(jīng)理是非常常見的一個問題。因為異常處理在項目設(shè)計中是非常關(guān)鍵的,所以精通異常處理是十分必要的。異常處理有很多最佳實踐,下面列舉集中,它們提高你代碼的健壯性和靈活性:
1) 調(diào)用方法的時候返回布爾值來代替返回null,這樣可以 NullPointerException。由于空指針是java異常里最惡心的異常
2) catch塊里別不寫代碼。空catch塊是異常處理里的錯誤事件,因為它只是捕獲了異常,卻沒有任何處理或者提示。通常你起碼要打印出異常信息,當然你最好根據(jù)需求對異常信息進行處理。
3)能拋受控異常(checked Exception)就盡量不拋受非控異常(checked Exception)。通過去掉重復(fù)的異常處理代碼,可以提高代碼的可讀性。
4) 絕對不要讓你的數(shù)據(jù)庫相關(guān)異常顯示到客戶端。由于絕大多數(shù)數(shù)據(jù)庫和SQLException異常都是受控異常,在Java中,你應(yīng)該在DAO層把異常信息處理,然后返回處理過的能讓用戶看懂并根據(jù)異常提示信息改正操作的異常信息。
5) 在Java中,一定要在數(shù)據(jù)庫連接,數(shù)據(jù)庫查詢,流處理后,在finally塊中調(diào)用close()方法。
5) 既然我們可以用RuntimeException來處理錯誤,那么你認為為什么Java中還存在檢查型異常?
這是一個有爭議的問題,在回答該問題時你應(yīng)當小心。雖然他們肯定愿意聽到你的觀點,但其實他們最感興趣的還是有說服力的理由。我認為其中一個理由是,存在檢查型異常是一個設(shè)計上的決定,受到了諸如C++等比Java更早編程語言設(shè)計經(jīng)驗的影響。絕大多數(shù)檢查型異常位于java.io包內(nèi),這是合乎情理的,因為在你請求了不存在的系統(tǒng)資源的時候,一段強壯的程序必須能夠優(yōu)雅的處理這種情況。通過把IOException聲明為檢查型異常,Java 確保了你能夠優(yōu)雅的對異常進行處理。另一個可能的理由是,可以使用catch或finally來確保數(shù)量受限的系統(tǒng)資源(比如文件描述符)在你使用后盡早得到釋放。 Joshua
Bloch編寫的 Effective Java 一書 中多處涉及到了該話題,值得一讀。
6) throw 和 throws這兩個關(guān)鍵字在java中有什么不同?
一個java初學(xué)者應(yīng)該掌握的面試問題。 throw 和 throws乍看起來是很相似的尤其是在你還是一個java初學(xué)者的時候。盡管他們看起來相似,都是在處理異常時候使用到的。但在代碼里的使用方法和用到的地方是不同的。throws總是出現(xiàn)在一個函數(shù)頭中,用來標明該成員函數(shù)可能拋出的各種異常, 你也可以申明未檢查的異常,但這不是編譯器強制的。如果方法拋出了異常那么調(diào)用這個方法的時候就需要將這個異常處理。另一個關(guān)鍵字 throw 是用來拋出任意異常的,按照語法你可以拋出任意 Throwable (i.e. Throwable
或任何Throwable的衍生類) , throw可以中斷程序運行,因此可以用來代替return . 最常見的例子是用 throw 在一個空方法中需要return的地方拋出 UnSupportedOperationException 代碼如下 :
可以看下這篇 文章查看這兩個關(guān)鍵字在java中更多的差異 。
7) 什么是“異常鏈”?
“異常鏈”是Java中非常流行的異常處理概念,是指在進行一個異常處理時拋出了另外一個異常,由此產(chǎn)生了一個異常鏈條。該技術(shù)大多用于將“ 受檢查異常” ( checked exception)封裝成為“非受檢查異常”(unchecked exception)或者RuntimeException。順便說一下,如果因為因為異常你決定拋出一個新的異常,你一定要包含原有的異常,這樣,處理程序才可以通過getCause()和initCause()方法來訪問異常最終的根源。
) 你曾經(jīng)自定義實現(xiàn)過異常嗎?怎么寫的?
很顯然,我們絕大多數(shù)都寫過自定義或者業(yè)務(wù)異常,像AccountNotFoundException。在面試過程中詢問這個Java異常問題的主要原因是去發(fā)現(xiàn)你如何使用這個特性的。這可以更準確和精致的去處理異常,當然這也跟你選擇checked 還是unchecked exception息息相關(guān)。通過為每一個特定的情況創(chuàng)建一個特定的異常,你就為調(diào)用者更好的處理異常提供了更好的選擇。相比通用異常(general exception),我更傾向更為精確的異常。大量的創(chuàng)建自定義異常會增加項目class的個數(shù),因此,在自定義異常和通用異常之間維持一個平衡是成功的關(guān)鍵。
9) JDK7中對異常處理做了什么改變?
這是最近新出的Java異常處理的面試題。JDK7中對錯誤(Error)和異常(Exception)處理主要新增加了2個特性,一是在一個catch塊中可以出來多個異常,就像原來用多個catch塊一樣。另一個是自動化資源管理(ARM), 也稱為try-with-resource塊。這2個特性都可以在處理異常時減少代碼量,同時提高代碼的可讀性。對于這些特性了解,不僅幫助開發(fā)者寫出更好的異常處理的代碼,也讓你在面試中顯的更突出。我推薦大家讀一下Java 7攻略,這樣可以更深入的了解這2個非常有用的特性。
10) 你遇到過 OutOfMemoryError 錯誤嘛?你是怎么搞定的?
這個面試題會在面試高級程序員的時候用,面試官想知道你是怎么處理這個危險的OutOfMemoryError錯誤的。必須承認的是,不管你做什么項目,你都會碰到這個問題。所以你要是說沒遇到過,面試官肯定不會買賬。要是你對這個問題不熟悉,甚至就是沒碰到過,而你又有3、4年的Java經(jīng)驗了,那么準備好處理這個問題吧。在回答這個問題的同時,你也可以借機向面試秀一下你處理內(nèi)存泄露、調(diào)優(yōu)和調(diào)試方面的牛逼技能。我發(fā)現(xiàn)掌握這些技術(shù)的人都能給面試官留下深刻的印象。
11) 如果執(zhí)行finally代碼塊之前方法返回了結(jié)果,或者JVM退出了,finally塊中的代碼還會執(zhí)行嗎?
這個問題也可以換個方式問:“如果在try或者finally的代碼塊中調(diào)用了System.exit(),結(jié)果會是怎樣”。了解finally塊是怎么執(zhí)行的,即使是try里面已經(jīng)使用了return返回結(jié)果的情況,對了解Java的異常處理都非常有價值。只有在try里面是有System.exit(0)來退出JVM的情況下finally塊中的代碼才不會執(zhí)行。
12)Java中final,finalize,finally關(guān)鍵字的區(qū)別
這是一個經(jīng)典的Java面試題了。我的一個朋友為Morgan Stanley招電信方面的核心Java開發(fā)人員的時候就問過這個問題。final和finally是Java的關(guān)鍵字,而finalize則是方法。final關(guān)鍵字在創(chuàng)建不可變的類的時候非常有用,只是聲明這個類是final的。而finalize()方法則是垃圾回收器在回收一個對象前調(diào)用,但也Java規(guī)范里面沒有保證這個方法一定會被調(diào)用。finally關(guān)鍵字是唯一一個和這篇文章討論到的異常處理相關(guān)的關(guān)鍵字。在你的產(chǎn)品代碼中,在關(guān)閉連接和資源文件的是時候都必須要用到finally塊。
參考文章
https://www.xuebuyuan.com/3248044.html
https://www.jianshu.com/p/49d2c3975c56
http://c.biancheng.net/view/1038.html
https://blog.csdn.net/Lisiluan/article/details/88745820
https://blog.csdn.net/michaelgo/article/details/82790253
微信公眾號
個人公眾號:程序員黃小斜
微信公眾號【程序員黃小斜】新生代青年聚集地,程序員成長充電站。作者黃小斜,職業(yè)是阿里程序員,身份是斜杠青年,希望和更多的程序員交朋友,一起進步和成長!專注于分享技術(shù)、面試、職場等成長干貨,這一次,我們一起出發(fā)。
關(guān)注公眾號后回復(fù)“2019”領(lǐng)取我這兩年整理的學(xué)習(xí)資料,涵蓋自學(xué)編程、求職面試、算法刷題、Java技術(shù)學(xué)習(xí)、計算機基礎(chǔ)和考研等8000G資料合集。
技術(shù)公眾號:Java技術(shù)江湖
微信公眾號【Java技術(shù)江湖】一位阿里 Java 工程師的技術(shù)小站,專注于 Java 相關(guān)技術(shù):SSM、SpringBoot、MySQL、分布式、中間件、集群、Linux、網(wǎng)絡(luò)、多線程,偶爾講點Docker、ELK,同時也分享技術(shù)干貨和學(xué)習(xí)經(jīng)驗,致力于Java全棧開發(fā)!
關(guān)注公眾號后回復(fù)“PDF”即可領(lǐng)取200+頁的《Java工程師面試指南》強烈推薦,幾乎涵蓋所有Java工程師必知必會的知識點。
總結(jié)
以上是生活随笔為你收集整理的夯实Java基础系列10:深入理解Java中的异常体系的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux下文件属性详解
- 下一篇: 金邦达:做最值得客户尊重的IC卡项目服务