[Java基本功] 一文搞懂Java中的異常機制
- 2019 年 10 月 7 日
- 筆記
本文非常詳盡地介紹了Java中的異常,幾乎360度無死角。
從異常的概念,分類,使用方法,注意事項和設計等方面全面地介紹了Java異常。
為什麼要使用異常
首先我們可以明確一點就是異常的處理機制可以確保我們程序的健壯性,提高系統可用率。雖然我們不是特別喜歡看到它,但是我們不能不承認它的地位,作用。
在沒有異常機制的時候我們是這樣處理的:通過函數的返回值來判斷是否發生了異常(這個返回值通常是已經約定好了的),調用該函數的程序負責檢查並且分析返回值。雖然可以解決異常問題,但是這樣做存在幾個缺陷:
1、 容易混淆。如果約定返回值為-11111時表示出現異常,那麼當程序最後的計算結果真的為-1111呢? 2、 代碼可讀性差。將異常處理代碼和程序代碼混淆在一起將會降低代碼的可讀性。 3、 由調用函數來分析異常,這要求程序員對庫函數有很深的了解。
在OO中提供的異常處理機制是提供代碼健壯的強有力的方式。使用異常機制它能夠降低錯誤處理代碼的複雜度,如果不使用異常,那麼就必須檢查特定的錯誤,並在程序中的許多地方去處理它。
而如果使用異常,那就不必在方法調用處進行檢查,因為異常機制將保證能夠捕獲這個錯誤,並且,只需在一個地方處理錯誤,即所謂的異常處理程序中。
這種方式不僅節約代碼,而且把「概述在正常執行過程中做什麼事」的代碼和「出了問題怎麼辦」的代碼相分離。總之,與以前的錯誤處理方法相比,異常機制使代碼的閱讀、編寫和調試工作更加井井有條。(摘自《Think in java 》)。
該部分內容選自http://www.cnblogs.com/chenssy/p/3438130.html
異常基本定義
在《Think in java》中是這樣定義異常的:異常情形是指阻止當前方法或者作用域繼續執行的問題。在這裡一定要明確一點:異常代碼某種程度的錯誤,儘管Java有異常處理機制,但是我們不能以「正常」的眼光來看待異常,異常處理機制的原因就是告訴你:這裡可能會或者已經產生了錯誤,您的程序出現了不正常的情況,可能會導致程序失敗! 那麼什麼時候才會出現異常呢?只有在你當前的環境下程序無法正常運行下去,也就是說程序已經無法來正確解決問題了,這時它所就會從當前環境中跳出,並拋出異常。拋出異常後,它首先會做幾件事。 首先,它會使用new創建一個異常對象,然後在產生異常的位置終止程序,並且從當前環境中彈出對異常對象的引用,這時。異常處理機制就會接管程序,並開始尋找一個恰當的地方來繼續執行程序,這個恰當的地方就是異常處理程序。 總的來說異常處理機制就是當程序發生異常時,它強制終止程序運行,記錄異常信息並將這些信息反饋給我們,由我們來確定是否處理異常。
異常體系
從上面這幅圖可以看出,Throwable是java語言中所有錯誤和異常的超類(萬物即可拋)。它有兩個子類:Error、Exception。
Java標準庫內建了一些通用的異常,這些類以Throwable為頂層父類。
Throwable又派生出Error類和Exception類。
錯誤:Error類以及他的子類的實例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理,Error很少出現。因此,程序員應該關注Exception為父類的分支下的各種異常類。
異常:Exception以及他的子類,代表程序運行時發送的各種不期望發生的事件。可以被Java異常處理機制使用,是異常處理的核心。
總體上我們根據Javac對異常的處理要求,將異常類分為2類。
非檢查異常(unckecked exception):Error 和 RuntimeException 以及他們的子類。javac在編譯時,不會提示和發現這樣的異常,不要求在程序處理這些異常。所以如果願意,我們可以編寫代碼處理(使用try…catch…finally)這樣的異常,也可以不處理。 對於這些異常,我們應該修正代碼,而不是去通過異常處理器處理 。這樣的異常發生的原因多半是代碼寫的有問題。如除0錯誤ArithmeticException,錯誤的強制類型轉換錯誤ClassCastException,數組索引越界ArrayIndexOutOfBoundsException,使用了空對象NullPointerException等等。 檢查異常(checked exception):除了Error 和 RuntimeException的其它異常。javac強制要求程序員為這樣的異常做預備處理工作(使用try…catch…finally或者throws)。在方法中要麼用try-catch語句捕獲它並處理,要麼用throws子句聲明拋出它,否則編譯不會通過。 這樣的異常一般是由程序的運行環境導致的。因為程序可能被運行在各種未知的環境下,而程序員無法干預用戶如何使用他編寫的程序,於是程序員就應該為這樣的異常時刻準備着。如SQLException , IOException,ClassNotFoundException 等。
需要明確的是:檢查和非檢查是對於javac來說的,這樣就很好理解和區分了。
這部分內容摘自http://www.importnew.com/26613.html
初識異常
異常是在執行某個函數時引發的,而函數又是層級調用,形成調用棧的,因為,只要一個函數發生了異常,那麼他的所有的caller都會被異常影響。當這些被影響的函數以異常信息輸出時,就形成的了異常追蹤棧。
異常最先發生的地方,叫做異常拋出點。
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)
從上面的例子可以看出,當devide函數發生除0異常時,devide函數將拋出ArithmeticException異常,因此調用他的CMDCalculate函數也無法正常完成,因此也發送異常,而CMDCalculate的caller——main 因為CMDCalculate拋出異常,也發生了異常,這樣一直向調用棧的棧底回溯。
這種行為叫做異常的冒泡,異常的冒泡是為了在當前發生異常的函數或者這個函數的caller中找到最近的異常處理程序。由於這個例子中沒有使用任何異常處理機制,因此異常最終由main函數拋給JRE,導致程序終止。
上面的代碼不使用異常處理機制,也可以順利編譯,因為2個異常都是非檢查異常。但是下面的例子就必須使用異常處理機制,因為異常是檢查異常。
代碼中我選擇使用throws聲明異常,讓函數的調用者去處理可能發生的異常。但是為什麼只throws了IOException呢?因為FileNotFoundException是IOException的子類,在處理範圍內。
異常和錯誤
下面看一個例子
//錯誤即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語句塊處理它。 或者,在函數簽名中使用throws 聲明交給函數調用者caller去解決。
下面看幾個具體的例子,包括error,exception和throwable
上面的例子是運行時異常,不需要顯示捕獲。 下面這個例子是可檢查異常需,要顯示捕獲或者拋出。
@Test public void testException() throws IOException { //FileInputStream的構造函數會拋出FileNotFoundException FileInputStream fileIn = new FileInputStream("E:\a.txt"); int word; //read方法會拋出IOException while((word = fileIn.read())!=-1) { System.out.print((char)word); } //close方法會拋出IOException fileIn.close(); }
一般情況下的處理方式 try catch finally
public class 異常處理方式 { @Test public void main() { try{ //try塊中放可能發生異常的代碼。 InputStream inputStream = new FileInputStream("a.txt"); //如果執行完try且不發生異常,則接着去執行finally塊和finally後面的代碼(如果有的話)。 int i = 1/0; //如果發生異常,則嘗試去匹配catch塊。 throw new SQLException(); //使用1.8jdk同時捕獲多個異常,runtimeexception也可以捕獲。只是捕獲後虛擬機也無法處理,所以不建議捕獲。 }catch(SQLException | IOException | ArrayIndexOutOfBoundsException exception){ System.out.println(exception.getMessage()); //每一個catch塊用於捕獲並處理一個特定的異常,或者這異常類型的子類。Java7中可以將多個異常聲明在一個catch中。 //catch後面的括號定義了異常類型和異常參數。如果異常與之匹配且是最先匹配到的,則虛擬機將使用這個catch塊來處理異常。 //在catch塊中可以使用這個塊的異常參數來獲取異常的相關信息。異常參數是這個catch塊中的局部變量,其它塊不能訪問。 //如果當前try塊中發生的異常在後續的所有catch中都沒捕獲到,則先去執行finally,然後到這個函數的外部caller中去匹配異常處理器。 //如果try中沒有發生異常,則所有的catch塊將被忽略。 }catch(Exception exception){ System.out.println(exception.getMessage()); //... }finally{ //finally塊通常是可選的。 //無論異常是否發生,異常是否匹配被處理,finally都會執行。 //finally主要做一些清理工作,如流的關閉,數據庫連接的關閉等。 }
一個try至少要跟一個catch或者finally
try { int i = 1; }finally { //一個try至少要有一個catch塊,否則, 至少要有1個finally塊。但是finally不是用來處理異常的,finally不會捕獲異常。 } }
異常出現時該方法後面的代碼不會運行,即使異常已經被捕獲。這裡舉出一個奇特的例子,在catch里再次使用try catch finally
@Test public void test() { try { throwE(); System.out.println("我前面拋出異常了"); System.out.println("我不會執行了"); } catch (StringIndexOutOfBoundsException e) { System.out.println(e.getCause()); }catch (Exception ex) { //在catch塊中仍然可以使用try catch finally try { throw new Exception(); }catch (Exception ee) { }finally { System.out.println("我所在的catch塊沒有執行,我也不會執行的"); } } } //在方法聲明中拋出的異常必須由調用方法處理或者繼續往上拋, // 當拋到jre時由於無法處理終止程序 public void throwE (){ // Socket socket = new Socket("127.0.0.1", 80); //手動拋出異常時,不會報錯,但是調用該方法的方法需要處理這個異常,否則會出錯。 // java.lang.StringIndexOutOfBoundsException // at com.javase.異常.異常處理方式.throwE(異常處理方式.java:75) // at com.javase.異常.異常處理方式.test(異常處理方式.java:62) throw new StringIndexOutOfBoundsException(); }
其實有的語言在遇到異常後仍然可以繼續運行
有的編程語言當異常被處理後,控制流會恢復到異常拋出點接着執行,這種策略叫做:resumption model of exception handling(恢複式異常處理模式 ) 而Java則是讓執行流恢復到處理了異常的catch塊後接着執行,這種策略叫做:termination model of exception handling(終結式異常處理模式)
"不負責任"的throws
throws是另一種處理異常的方式,它不同於try…catch…finally,throws僅僅是將函數中可能出現的異常向調用者聲明,而自己則不具體處理。
採取這種異常處理的原因可能是:方法本身不知道如何處理這樣的異常,或者說讓調用者處理更好,調用者需要為可能發生的異常負責。
public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN { //foo內部可以拋出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 類的異常,或者他們的子類的異常對象。 }
糾結的finally
finally塊不管異常是否發生,只要對應的try執行了,則它一定也執行。只有一種方法讓finally塊不執行:System.exit()。因此finally塊通常用來做資源釋放操作:關閉文件,關閉數據庫連接等等。
良好的編程習慣是:在try塊中打開資源,在finally塊中清理釋放這些資源。
需要注意的地方:
1、finally塊沒有處理異常的能力。處理異常的只能是catch塊。
2、在同一try…catch…finally塊中 ,如果try中拋出異常,且有匹配的catch塊,則先執行catch塊,再執行finally塊。如果沒有catch塊匹配,則先執行finally,然後去外面的調用者中尋找合適的catch塊。
3、在同一try…catch…finally塊中 ,try發生異常,且匹配的catch塊中處理異常時也拋出異常,那麼後面的finally也會執行:首先執行finally塊,然後去外圍調用者中尋找合適的catch塊。
public class finally使用 { public static void main(String[] args) { try { throw new IllegalAccessException(); }catch (IllegalAccessException e) { // throw new Throwable(); //此時如果再拋異常,finally無法執行,只能報錯。 //finally無論何時都會執行 //除非我顯示調用。此時finally才不會執行 System.exit(0); }finally { System.out.println("算你狠"); } } }
throw : JRE也使用的關鍵字
throw exceptionObject
程序員也可以通過throw語句手動顯式的拋出一個異常。throw語句的後面必須是一個異常對象。
throw 語句必須寫在函數中,執行throw 語句的地方就是一個異常拋出點,==它和由JRE自動形成的異常拋出點沒有任何差別。==
public void save(User user) { if(user == null) throw new IllegalArgumentException("User對象為空"); //...... }
後面開始的大部分內容都摘自http://www.cnblogs.com/lulipro/p/7504267.html
該文章寫的十分細緻到位,令人欽佩,是我目前為之看到關於異常最詳盡的文章,可以說是站在巨人的肩膀上了。
異常調用鏈
異常的鏈化
在一些大型的,模塊化的軟件開發中,一旦一個地方發生異常,則如骨牌效應一樣,將導致一連串的異常。假設B模塊完成自己的邏輯需要調用A模塊的方法,如果A模塊發生異常,則B也將不能完成而發生異常。
==但是B在拋出異常時,會將A的異常信息掩蓋掉,這將使得異常的根源信息丟失。異常的鏈化可以將多個模塊的異常串聯起來,使得異常信息不會丟失。==
異常鏈化:以一個異常對象為參數構造新的異常對象。新的異對象將包含先前異常的信息。這項技術主要是異常類的一個帶Throwable參數的函數來實現的。這個當做參數的異常,我們叫他根源異常(cause)。
查看Throwable類源碼,可以發現裏面有一個Throwable字段cause,就是它保存了構造時傳遞的根源異常參數。這種設計和鏈表的結點類設計如出一轍,因此形成鏈也是自然的了。
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 異常鏈 { @Test public void test() { C(); } public void A () throws Exception { try { int i = 1; i = i / 0; //當我注釋掉這行代碼並使用B方法拋出一個error時,運行結果如下 // 四月 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類的構造方法將最底層的異常重新包裝並拋出,此時注入了A方法的信息。最後打印棧信息時可以看到caused by A方法的異常。 //如果直接拋出,棧信息打印結果只能看到上層方法的錯誤信息,不能看到其實是A發生了錯誤。 //所以需要包裝並拋出 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(); } } //最後結果 // 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。
按照國際慣例,自定義的異常應該總是包含如下的構造函數:
一個無參構造函數 一個帶有String參數的構造函數,並傳遞給父類的構造函數。 一個帶有String參數和Throwable參數,並都傳遞給父類構造函數 一個帶有Throwable 參數的構造函數,並傳遞給父類的構造函數。 下面是IOException類的完整源代碼,可以借鑒。
public class IOException extends Exception { static final long serialVersionUID = 7818375828146090155L; public IOException() { super(); } public IOException(String message) { super(message); } public IOException(String message, Throwable cause) { super(message, cause); } public IOException(Throwable cause) { super(cause); } }
異常的注意事項
異常的注意事項
當子類重寫父類的帶有 throws聲明的函數時,其throws聲明的異常必須在父類異常的可控範圍內——用於處理父類的throws方法的異常處理器,必須也適用於子類的這個帶throws方法 。這是為了支持多態。 例如,父類方法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(); } }
/******假設上面的代碼是允許的(實質是錯誤的)*******/
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類拋出的實質是SQLException,而IOException無法處理它。 //那麼這裡的try。。catch就不能處理Son中的異常。 //多態就不能實現了。 try { obj.start(); }catch(IOException) { //處理IOException } } } }
==Java的異常執行流程是線程獨立的,線程之間沒有影響==
Java程序可以是多線程的。每一個線程都是一個獨立的執行流,獨立的函數調用棧。如果程序只有一個線程,那麼沒有被任何代碼處理的異常 會導致程序終止。如果是多線程的,那麼沒有被任何代碼處理的異常僅僅會導致異常所在的線程結束。 也就是說,Java中的異常是線程獨立的,線程的問題應該由線程自己來解決,而不要委託到外部,也不會直接影響到其它線程的執行。
下面看一個例子
public class 多線程的異常 { @Test public 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 //每個線程拋出異常時並不會影響其他線程的繼續執行 public void run() { try { System.out.println("start thread" + finalI); throw new Exception(); }catch (Exception e) { System.out.println("thread" + finalI + " go wrong"); } } }); } // 結果: // start thread0 // thread0 go wrong // start thread1 // thread1 go wrong // start thread2 // thread2 go wrong } }
當finally遇上return
首先一個不容易理解的事實:
在 try塊中即便有return,break,continue等改變執行流的語句,finally也會執行。
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 */
很多人面對這個問題時,總是在歸納執行的順序和規律,不過我覺得還是很難理解。我自己總結了一個方法。用如下GIF圖說明。
也就是說:try…catch…finally中的return 只要能執行,就都執行了,他們共同向同一個內存地址(假設地址是0×80)寫入返回值,後執行的將覆蓋先執行的數據,而真正被調用者取的返回值就是最後一次寫入的。那麼,按照這個思想,下面的這個例子也就不難理解了。
finally中的return 會覆蓋 try 或者catch中的返回值。
public static void main(String[] args) { int result; result = foo(); System.out.println(result); /////////2 result = 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的任務,不要在finally中做一些其它的事情,finally塊僅僅用來釋放資源是最合適的。 將盡量將所有的return寫在函數的最後面,而不是try … catch … finally中。