程序運行時,發生的不被指望的事件,它阻止了程序按照程序員的預期正常執行,這就是異常。異常發生時,是任程序自生自滅,馬上退出終止,仍是輸出錯誤給給用戶?或者用 C 語言風格:用函數返回值做爲執行狀態?
Java 提供了更加優秀解決辦法:異常處理機制。java
異常處理機制能讓程序在異常發生時,按照代碼預先設定的異常處理邏輯,針對性地處理異常,讓程序盡最大可能恢復正常並繼續執行,且保持代碼的清晰。
Java 中的異常能夠是函數中語句執行時引起的,也能夠是程序員經過 throw 語句手動拋出的,只要在 Java 程序中產生了異常,就會用一個對應類型的異常對象來封裝異常,JRE 就會試圖尋找異常處理程序來處理異常。程序員
Throwable 類是 Java 異常類型的頂層父類,一個對象只有是 Throwable 類的(直接或間接)實例,它纔是一個異常對象,才能被異常處理機制識別。JDK 中內建了一些經常使用的異常類,咱們也能夠自定義異常。數據庫
Java 標準庫內建了一些通用的異常,這些類以 Throwable 爲頂層父類。
Throwable 又派生出 Error 類和 Exception 類。編程
錯誤:Error 類以及它的子類的實例,表明 JVM 自己的錯誤。錯誤不能被程序員經過代碼處理,Error 不多出現。所以,程序員應該關注 Exception 爲父類的分支下的各類異常類。數組
異常:Exception 以及它的子類,表明程序運行時發送的各類不被指望發生的事件。能夠被 Java 異常處理機制使用,是異常處理的核心。 多線程
整體上咱們根據 Java 對異常的處理要求,將異常分爲兩類。編程語言
非檢查異常(unchecked exception)
Error 和 RuntimeException 以及它們的子類。Java 在編譯時,不會提示和發現這樣的異常,不要求在程序中處理這些異常。因此若是願意,咱們能夠編寫代碼處理(使用 try...catch...finally)這樣的異常,也能夠不處理。對於這些異常,咱們更應該的不是去處理這些異常,而是應該修正代碼。這樣的異常發生的緣由多半是代碼邏輯寫的有問題。如除 0 錯誤 ArithmeticException,錯誤的強制類型轉換錯誤 ClassCastException,數組索引越界錯誤 ArrayIndexOutOfBoundsException,操做了空對象錯誤 NullPointerException 等等。ide
檢查異常(checked exception)
除了 Error 和 RuntimeException 的其它異常。Java 強制要求程序員爲這樣的異常作預備處理工做(使用 try...catch...finally 或者 throws)。在方法中要麼用 try...catch 語句捕獲它並處理,要麼用 throws 子句聲明拋出它,不然編譯不會經過。這樣的異常通常是由程序的運行環境致使的。由於程序可能運行被運行在各類未知的環境下,而程序員沒法干預用戶如何使用他編寫的程序,因而程序員就應該爲這樣的異常時刻準備着。如 SQLException,IOException,ClassNotFoundException 等。模塊化
下面的代碼會演示2個異常類型:ArithmeticException 和 InputMimatchException。前者因爲整數除 0 引起,後者是輸入的數據不能被轉化爲 int 類型引起。函數
package com.example; import java. util .Scanner ; public class AllDemo { 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 ; } } /***************************************** ----歡迎使用命令行除法計算器---- 0 Exception in thread "main" java.lang.ArithmeticException : / by zero at com.example.AllDemo.devide( AllDemo.java:30 ) at com.example.AllDemo.CMDCalculate( AllDemo.java:22 ) at com.example.AllDemo.main( AllDemo.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.example.AllDemo.CMDCalculate( AllDemo.java:20 ) at com.example.AllDemo.main( AllDemo.java:12 ) *****************************************/
異常是在執行某個函數時引起的,而函數又是層級調用,造成調用棧的,所以,只要一個函數發生了異常,那麼它的全部 caller 都會被異常影響。當這些被影響的函數以異常信息輸出時,就造成了異常追蹤棧。
異常最早發生的地方,叫作異常拋出點。
從上面的例子能夠看出,當 devide 函數發生除 0 異常時,devide 函數拋出 ArithmeticExcepton 異常,所以調用它的 CMDCalculate 函數也沒法正常完成,所以也發送異常,而 CMDCalculate 的 caller --main 由於 CMDCalculate 拋出異常,也發生了異常,這樣一直向調用棧的棧底回溯。這種行爲叫作異常的冒泡,異常的冒泡是爲了在當前發生異常的函數或者這個函數的 caller 中找到最近的異常處理程序。因爲這個例子沒有使用任何異常處理機制,所以異常最終由 main 函數拋給 JRE,致使程序終止。
上面的代碼不使用異常處理機制,也能夠順利編譯,由於2個異常都是非檢查異常。可是下面的例子就必須使用異常處理機制,由於異常是檢查異常。
代碼中選擇使用 throws 聲明異常,讓函數的調用者去處理可能發生的異常。可是爲何只 throws 了 IOException呢?由於 FileNotFoundException 是 IOException 的子類,在處理範圍內。
@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.clos }
在編寫代碼處理異常時,對於檢查異常,有2中不一樣的處理方式:使用 try...catch...finally 語句塊處理它。或者,在函數簽名中使用 throws 聲明交給函數調用者 caller 去處理。
try...catch...finally 語句塊
try{ //try塊中放可能發生異常的代碼。 //若是執行完try且不發生異常,則接着去執行finally塊和finally後面的代碼(若是有的話)。 //若是發生異常,則嘗試去匹配catch塊。 }catch(SQLException SQLexception){ //每個catch塊用於捕獲並處理一個特定的異常,或者這異常類型的子類。Java7中能夠將多個異常聲明在一個catch中。 //catch後面的括號定義了異常類型和異常參數。若是異常與之匹配且是最早匹配到的,則虛擬機將使用這個catch塊來處理異常。 //在catch塊中可使用這個塊的異常參數來獲取異常的相關信息。異常參數是這個catch塊中的局部變量,其它塊不能訪問。 //若是當前try塊中發生的異常在後續的全部catch中都沒捕獲到,則先去執行finally,而後到這個函數的外部caller中去匹配異常處理器。 //若是try中沒有發生異常,則全部的catch塊將被忽略。 }catch(Exception exception){ //... }finally{ //finally塊一般是可選的。 //不管異常是否發生,異常是否匹配被處理,finally都會執行。 //一個try至少要有一個catch塊,不然, 至少要有1個finally塊。可是finally不是用來處理異常的,finally不會捕獲異常。 //finally主要作一些清理工做,如流的關閉,數據庫鏈接的關閉等。 }
注:
有的編程語言當異常被處理後,控制流會恢復到異常拋出點接着執行,這種策略叫作:resumption model of exception handing(恢復式異常處理模式)
而 Java 則是讓執行流恢復處處理了異常的 catch 塊後接着執行,這種策略叫作:termination model of exception handing(終結式異常處理模式)
public static void main(String[] args){ try { foo(); }catch(ArithmeticException ae) { System.out.println("處理異常"); } } public static void foo(){ int a = 5/0; //異常拋出點 System.out.println("爲何還不給我漲工資!!!"); //////////////////////不會執行 }
throws 函數聲明
throws 聲明:若是一個方法內部的代碼會拋出檢查異常(checked exception),而方法本身又沒有徹底處理掉,則 Java 保證你必須在方法的簽名上使用 throws 關鍵字聲明這些可能拋出的異常,不然編譯不經過。
throws 時另外一種處理異常的方式,它不一樣於 try...catch...finally,throws 僅僅時將函數中可能出現的異常向調用者聲明,而本身則不具體處理。
採起這種異常處理的緣由多是:方法自己不知道如何處理這樣的異常,或者說讓調用者處理更好,調用者須要爲可能發生的異常負責。
finally 塊無論異常是否發生,只要對應的 try 執行了,則它必定也執行。只有一種方法可讓 finally 塊不執行:System.exit(0)
。所以 finally 塊一般用來作資源釋放操做:關閉文件,關閉數據庫鏈接等等。
注:
程序員也能夠經過 throw 語句手動顯示的拋出一個異常。throw 語句的後面必須是一個異常對象。
throw 語句必須寫在函數中,執行 throw 語句的地方就是一個異常拋出點,它和有 JRE 自動造成的異常拋出點沒有任何差異。
public void save(User user) { if(user == null) throw new IllegalArgumentException("User對象爲空"); //...... }
在一些大型的,模塊化的軟甲開發中,一旦一個地方發生異常,則如骨牌效應通常,將致使一連串的異常。假設 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; } //........ }
若是要自定義異常,則擴展 Exception 類便可,所以這樣的自定義異常都屬於檢查異常。若是要自定義非檢查異常,則擴展自 RuntimeException。
自定義的異常應該老是包含以下的構造函數:
下面是 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); } }
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 } } } }
首先一個不容易理解的事實:在 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 */
也就是說:try...catch.finally 中的 return 只要能執行,就都執行了,它們共同向同一個內存地址(假設地址是 0x80)寫入返回值,後執行的將覆蓋先執行的數據,而真正被調用者取的返回值就是最後一次寫入的。那麼,按照這個思想,下面的這個例子也就不難理解了。
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個例子都異於常人的編碼思惟,所以建議: