本文嘗試以儘量詳細的方式介紹 Java 當中的異常概念和處理機制。本文適合 Java 初學者閱讀。java
異常是發生在程序運行過程當中的,阻斷正常流程中的指令執行的事件。segmentfault
當一個方法在執行當中發生錯誤時,這個方法就會建立一個特別的對象,將其交付給 Java 運行環境處理。這個對象被稱做「異常對象」(或簡稱異常),它當中包含了異常事件的類型和狀態等等信息。 建立這個對象並將其交付給 Java 運行環境的過程,稱做「拋出異常」。指針
異常對象:
全部的異常對象都繼承於java.lang.Exception
類。不一樣類型的異常被定義成它的不一樣子類。常見的異常類型有java.io.IOException
(讀寫異常),java.lang.NullPointerException
(空指針異常)等。code
當一個方法拋出異常(或「異常對象」)時,運行環境就會嘗試尋找某個方法去處理它。全部可以處理該異常的方法,都來自一個叫作「調用堆棧」的方法列表。這個列表的最頂層就是拋出該異常的方法,往下是調用該方法的方法,而後依次類推。對象
例如a()
方法當中調用了b()
方法,b()
方法當中調用了c()
方法,那麼當c()
方法拋出異常時,它就在調用堆棧的最頂層,往下是b()
方法,再往下就是a()
方法。blog
運行環境會沿着調用堆棧往下尋找,尋找方法當中是否存在可以處理這個異常的代碼塊。這樣的代碼塊叫作「捕獲異常」。運行環境將異常對象交給這個「捕獲異常」的代碼塊處理。若是運行環境在調用堆棧中自始至終未能找到捕獲這個異常的代碼塊,那麼整個程序將終止運行。繼承
當你編寫一個方法 a(),當中調用了方法 b(),而該方法可能拋出異常,那麼你有兩種選擇:事件
1、在 a() 方法中用 try-catch 結構捕獲 b() 方法拋出的異常,方式以下:get
// 示例1 try { ... b(); ... } catch (Exception e) { // 處理異常對象 }
2、在 a() 方法上聲明爲拋出 b() 方法所拋出的異常,像這樣:編譯器
// 示例2 public void a() throws Exception { ... b(); ... }
這樣的話,調用 a() 方法的那個方法,一樣面臨兩種選擇:捕獲,或繼續拋出。若是你不在這兩種處理方式當中選擇一種,那麼編譯器就會斷定你的代碼存在錯誤,並拒絕編譯。
開始的時候說過,異常事件會阻斷指令的執行。也就是說,執行到 b() 方法的時候,若是它拋出了異常,那麼它後面的指令都不會執行了。好比在第一種狀況下(示例 1)拋出異常,那麼從 b();
到 } catch (Exception e) {
之間的全部語句都不會執行;在第二種狀況下(示例 2)則是從 b();
到 a() 方法結束之間的全部語句都不會執行。
接下來咱們分別介紹這兩種處理方式。
在 Java 中,捕獲異常的方式是編寫 try-catch-finally 代碼塊。下面是一個例子,讀取指定文件並將文件內容輸出到控制檯:
// 示例3 public static void main(String[] args) { java.io.File file = new File("C:\\1.txt"); java.io.BufferedReader reader = null; String line; try { reader = new BufferedReader( new java.io.FileReader(file)); // 1 while ((line = reader.readLine()) != null) { // 2 System.out.println(line); } } catch (IOException e) { // 3 e.printStackTrace(); } finally { // 4 if (reader != null) { try { reader.close(); // 5 } catch (Exception e) { e.printStackTrace(); } } } }
上面的例子中,1 處的 new FileReader(file)
和 [2] 處的 readLine()
方法均可能拋出 java.io.IOException
,因此咱們用 try {...}
將它們包起來,表示這部分代碼執行的時候可能拋出異常。
[3] 處爲 catch 塊,這部分決定了當異常真的發生時,該如何處理。假如 1 處拋出了異常,那麼 Java 運行環境將跳過 try 塊後面的 while 部分,直接轉到 catch 塊來處理這個異常。
catch 後面的 (IOException e)
聲明瞭被捕獲的異常對象,你能夠在 catch 塊中使用它。在這裏咱們簡單的調用 e.printStackTrace()
方法,將異常信息輸出到控制檯。
注意,try 塊中拋出的異常類型必須與 catch 塊聲明所要捕獲的異常類型匹配,不然編譯器將拒絕編譯。關於這一點的詳情將在後面說明。
[4] 處爲 finally 塊,這部分決定了當 try 塊和/或 catch 塊執行完畢後,還要執行哪些代碼,這部分能夠看做是「老是會執行的」、「善後工做」。在這裏咱們嘗試關閉 reader 對象,不管是否出現異常。
注意 reader.close();
方法也可能拋出異常,因此 [5] 這裏又須要用 try-catch-finally 塊包起來。你固然已經注意到,這裏沒有 finally 部分。其實 try-catch-finally 塊有三種寫法,分別是:
1. try {...A...} catch(Exception e) {...B...} finally {...C...} 2. try {...A...} catch(Exception e) {...B...} 3. try {...A...} finally {...C...}
寫法 1 的意思是:嘗試 A;若是出現異常則執行 B;不論是否出現異常,都執行 C。
寫法 2 的意思是:嘗試 A;若是出現異常則執行 B。
寫法 3 的意思是:嘗試 A;若是出現異常則不理會拋出去;不論是否出現異常,都執行 C。
在寫法 3 中,若是 try 部分拋出了異常,則須要在當前方法中聲明。聲明拋出異常的方式,將在接下來講明。
任何一個方法均可以經過 throws 關鍵字聲明本身可能拋出異常。下面是一個例子:
// 示例4 public void f() throws Exception { ... }
在這個例子中,方法 f()
聲明本身可能會拋出 java.lang.Exception
類型的異常。除了 Exception 外,方法也能夠明確的指出本身可能拋出哪一種類型的異常,例如:
// 示例5 public void f() throws java.io.IOException { ... }
若是方法當中的某條語句可能會拋出 A 類型的異常,那麼方法就不能聲明爲拋出 B 類型的異常。下面的作法是錯誤的:
// 示例6 public void g() throws java.lang.NullPointerException { f(); }
由於 f()
已經聲明爲拋出 IOException,而 g()
並無將該異常捕獲,所以它也必須聲明爲 「throws IOException」。若是編譯器發現這種不匹配的狀況,就會拒絕編譯。
有一種狀況例外,就是 B 類型的異常是 A 類型的異常的父類。這意味着 B 類型的異常在業務邏輯上涵蓋了 A 類型,因此咱們相信凡是能處理 B 的天然也能處理 A。
所以,既然 java.lang.Exception
是全部異常的父類,那麼凡是聲明拋出該類型異常的方法,天然能夠拋出任意類型的異常。也就是說,當一個方法聲明爲 「throws Exception」 時,它能夠拋出任何類型的異常。注意,由於這種聲明方式會給方法的調用者帶來困擾,因此咱們儘可能不要這麼作。
回顧一下示例 3:
如今回顧一下示例 3 中catch(IOException e) {...}
部分。若是將它改成catch(Exception e) {...}
,就表示捕獲全部類型的異常,即它前面的 try 部分不論拋出什麼類型的異常,它均可以處理。某些狀況下這樣作是合理的,由於咱們不但願當遇到某個特定種類的異常時,程序終止運行。