深刻理解Java異常

引言

說到異常,你們腦海中第一反應確定是try-catch-finally這樣的固定的組合。的確,這是Java異常處理的基本範式,下面咱們就來好好聊聊Java異常機制,看看這個背後還有哪些咱們忽略的細節。java

Java異常介紹

異常時什麼?就是指阻止當前方法或做用域繼續執行的問題,當程序運行時出現異常時,系統就會自動生成一個Exception對象來通知程序進行相應的處理。Java異常的類型有不少種,下面咱們就使用一張圖來看一下Java異常的繼承層次結構:數組

圖片中咱們看到Java異常的基類是Throwable類型,而後它有兩個派生類Error和Exception類型,而後Exception類有分爲受檢查異常及RuntimeException(運行時異常)。下面咱們就來逐一介紹。

Java異常中的Error

Error通常表示編譯時或者系統錯誤,例如:虛擬機相關的錯誤,系統崩潰(例如:咱們開發中有時會遇到的OutOfMemoryError)等。這種錯誤沒法恢復或不可捕獲,將致使應用程序中斷,一般應用程序沒法處理這些錯誤,所以也不該該試圖用catch來進行捕獲。安全

Java異常中的Exception

上面咱們有介紹,Java異常的中的Exception分爲受檢查異常和運行時異常(不受檢查異常)。下面咱們展開介紹。bash

Java中的受檢查異常

相信你們在寫IO操做的代碼的時候,必定有過這樣的記憶,對File或者Stream進行操做的時候必定須要使用try-catch包起來,不然編譯會失敗,這是由於這些異常類型是受檢查的異常類型。編譯器在編譯時,對於受檢異常必須進行try...catch或throws處理,不然沒法經過編譯。常見的受檢查異常包括:IO操做、ClassNotFoundException、線程操做等。網絡

Java中的非受檢查異常(運行時異常)

RuntimeException及其子類都統稱爲非受檢查異常,例如:NullPointExecrption、NumberFormatException(字符串轉換爲數字)、ArrayIndexOutOfBoundsException(數組越界)、ClassCastException(類型轉換錯誤)、ArithmeticException(算術錯誤)等。ide

Java的異常處理

Java處理異常的通常格式是這樣的:函數

try{
    ///可能會拋出異常的代碼
}catch(Type1 id1){
    //處理Type1類型異常的代碼
}catch(Type2 id2){
    //處理Type2類型異常的代碼
}
複製代碼

try塊中放置可能會發生異常的代碼(可是咱們不知道具體會發生哪一種異常)。若是異常發生了,try塊拋出系統自動生成的異常對象,而後異常處理機制將負責搜尋參數與異常類型相匹配的第一個處理程序,而後進行catch語句執行(不會在向下查找)。若是咱們的catch語句沒有匹配到,那麼JVM虛擬機仍是會拋出異常的。ui

Java中的throws關鍵字

若是在當前方法不知道該如何處理該異常時,則可使用throws對異常進行拋出給調用者處理或者交給JVM。JVM對異常的處理方式是:打印異常的跟蹤棧信息並終止程序運行。 throws在使用時應處於方法簽名以後使用,能夠拋出多種異常並用英文字符逗號’,’隔開。下面是一個例子:spa

public void f() throws ClassNotFoundException,IOException{}
複製代碼

這樣咱們調用f()方法的時候必需要catch-ClassNotFoundException和IOException這兩個異常或者catch-Exception基類。
注意:
throws的這種使用方式只是Java編譯期要求咱們這樣作的,咱們徹底能夠只在方法聲明中throws相關異常,可是在方法裏面卻不拋出任何異常,這樣也能經過編譯,咱們經過這種方式間接的繞過了Java編譯期的檢查。這種方式有一個好處:爲異常先佔一個位置,之後就能夠拋出這種異常而不須要修改已有的代碼。在定義抽象類和接口的時候這種設計很重要,這樣派生類或者接口實現就能夠拋出這些預先聲明的異常。線程

打印異常信息

異常類的基類Exception中提供了一組方法用來獲取異常的一些信息.因此若是咱們得到了一個異常對象,那麼咱們就能夠打印出一些有用的信息,最經常使用的就是void printStackTrace()這個方法,這個方法將返回一個由棧軌跡中的元素所構成的數組,其中每一個元素都表示棧中的一幀.元素0是棧頂元素,而且是調用序列中的最後一個方法調用(這個異常被建立和拋出之處);他有幾個不一樣的重載版本,能夠將信息輸出到不一樣的流中去.下面的代碼顯示瞭如何打印基本的異常信息:

public void f() throws IOException{
    System.out.println("Throws SimpleException from f()"); 
    throw new IOException("Crash");
 }
 public static void main(String[] agrs) {
    try {
    	new B().f();
    } catch (IOException e) {
    	System.out.println("Caught Exception");
        System.out.println("getMessage(): "+e.getMessage());
        System.out.println("getLocalizedMessage(): "+e.getLocalizedMessage());
        System.out.println("toString(): "+e.toString());
        System.out.println("printStackTrace(): ");
        e.printStackTrace(System.out);
    }
}
複製代碼

咱們來看輸出:

Throws SimpleException from f()
Caught  Exception
getMessage(): Crash
getLocalizedMessage(): Crash
toString(): java.io.IOException: Crash
printStackTrace(): 
java.io.IOException: Crash
	at com.learn.example.B.f(RunMain.java:19)
	at com.learn.example.RunMain.main(RunMain.java:26)
複製代碼

使用finally進行清理

引入finally語句的緣由是咱們但願一些代碼老是能獲得執行,不管try塊中是否拋出異常.這樣異常處理的基本格式變成了下面這樣:

try{
    //可能會拋出異常的代碼
}
catch(Type1 id1){
    //處理Type1類型異常的代碼
}
catch(Type2 id2){
    //處理Type2類型異常的代碼
}
finally{
    //老是會執行的代碼
}
複製代碼

在Java中但願除內存之外的資源恢復到它們的初始狀態的時候須要使用的finally語句。例如打開的文件或者網絡鏈接,屏幕上的繪製的圖像等。下面咱們來看一下案例:

public class FinallyException {
    static int count = 0;

    public static void main(String[] args) {
        while (true){
            try {
                if (count++ == 0){
                    throw new ThreeException();
                }
                System.out.println("no Exception");
            }catch (ThreeException e){
                System.out.println("ThreeException");
            }finally {
                System.out.println("in finally cause");
                if(count == 2)
                    break;
            }
        }
    }
}

class ThreeException extends Exception{}
複製代碼

咱們來看輸出:

ThreeException
in finally cause
no Exception
in finally cause
複製代碼

若是咱們在try塊或者catch塊裏面有return語句的話,那麼finally語句還會執行嗎?咱們看下面的例子:

public class MultipleReturns {
    public static void f(int i){
        System.out.println("start.......");
        try {
            System.out.println("1");
            if(i == 1)
                return;
            System.out.println("2");
            if (i == 2)
                return;
            System.out.println("3");
            if(i == 3)
                return;
            System.out.println("else");
            return;
        }finally {
            System.out.println("end");
        }
    }

    public static void main(String[] args) {
        for (int i = 1; i<4; i++){
            f(i);
        }
    }
}
複製代碼

咱們來看運行結果:

start.......
1
end
start.......
1
2
end
start.......
1
2
3
end
複製代碼

咱們看到即便咱們在try或者catch塊中使用了return語句,finally子句仍是會執行。那麼有什麼狀況finally子句不會執行呢?
有下面兩種狀況會致使Java異常的丟失

  • finally中重寫拋出異常(finally中重寫拋出另外一種異常會覆蓋原來捕捉到的異常)
  • 在finally子句中返回(即return)

Java異常棧

前面稍微提到了點Java異常棧的相關內容,這一節咱們經過一個簡單的例子來更加直觀的瞭解異常棧的相關內容。咱們再看Exception異常的時候會發現,發生異常的方法會在最上層,main方法會在最下層,中間還有其餘的調用層次。這實際上是棧的結構,先進後出的。下面咱們經過例子來看下:

public class WhoCalled {
    static void f() {
        try {
            throw new Exception();
        } catch (Exception e) {
            for (StackTraceElement ste : e.getStackTrace()){
                System.out.println(ste.getMethodName());
            }
        }
    }

    static void g(){
        f();
    }

    static void h(){
        g();
    }

    public static void main(String[] args) {
        f();
        System.out.println("---------------------------");
        g();
        System.out.println("---------------------------");
        h();
        System.out.println("---------------------------");

    }
}
複製代碼

咱們來看輸出結果:

f
main
---------------------------
f
g
main
---------------------------
f
g
h
main
---------------------------
複製代碼

能夠看到異常信息都是從內到外的,按個人理解查看異常的時候要從第一條異常信息看起,由於那是異常發生的源頭。

從新拋出異常及異常鏈

咱們知道每遇到一個異常信息,咱們都須要進行try…catch,一個還好,若是出現多個異常呢?分類處理確定會比較麻煩,那就一個Exception解決全部的異常吧。這樣確實是能夠,可是這樣處理勢必會致使後面的維護難度增長。最好的辦法就是將這些異常信息封裝,而後捕獲咱們的封裝類便可。
咱們有兩種方式處理異常,一是throws拋出交給上級處理,二是try…catch作具體處理。可是這個與上面有什麼關聯呢?try…catch的catch塊咱們能夠不須要作任何處理,僅僅只用throw這個關鍵字將咱們封裝異常信息主動拋出來。而後在經過關鍵字throws繼續拋出該方法異常。它的上層也能夠作這樣的處理,以此類推就會產生一條由異常構成的異常鏈。
經過使用異常鏈,咱們能夠提升代碼的可理解性、系統的可維護性和友好性。
咱們捕獲異常之後通常會有兩種操做

  • 捕獲後拋出原來的異常,但願保留最新的異常拋出點--fillStackTrace
  • 捕獲後拋出新的異常,但願拋出完整的異常鏈--initCause

捕獲異常後從新拋出異常

在函數中捕獲了異常,在catch模塊中不作進一步的處理,而是向上一級進行傳遞 catch(Exception e){ throw e;},咱們經過例子來看一下:

public class ReThrow {
    public static void f()throws Exception{
        throw new Exception("Exception: f()");
    }

    public static void g() throws Exception{
        try{
            f();
        }catch(Exception e){
            System.out.println("inside g()");
            throw e;
        }
    }
    public static void main(String[] args){
        try{
            g();
        }
        catch(Exception e){
            System.out.println("inside main()");
            e.printStackTrace(System.out);
        }
    }
}
複製代碼

咱們來看輸出:

inside g()
inside main()
java.lang.Exception: Exception: f()
        //異常的拋出點仍是最初拋出異常的函數f()
	at com.learn.example.ReThrow.f(RunMain.java:5)
	at com.learn.example.ReThrow.g(RunMain.java:10)
	at com.learn.example.RunMain.main(RunMain.java:21)
複製代碼

fillStackTrace——覆蓋前邊的異常拋出點(獲取最新的異常拋出點)

在此拋出異常的時候進行設置 catch(Exception e){ (Exception)e.fillInStackTrace();} 咱們經過例子看一下:(仍是剛纔的例子)

public void g() throws Exception{
    try{
        f();
    }catch(Exception e){
    	System.out.println("inside g()");
        throw (Exception)e.fillInStackTrace();
    }
}
複製代碼

運行結果以下:

inside g()
inside main()
java.lang.Exception: Exception: f()
        //顯示的就是最新的拋出點
	at com.learn.example.ReThrow.g(RunMain.java:13)
	at com.learn.example.RunMain.main(RunMain.java:21)
複製代碼

捕獲異常後拋出新的異常(保留原來的異常信息,區別於捕獲異常以後從新拋出)

若是咱們在拋出異常的時候須要保留原來的異常信息,那麼有兩種方式

  • 方式1:Exception e=new Exception(); e.initCause(ex);
  • 方式2:Exception e =new Exception(ex);
class ReThrow {
    public void f(){
        try{
             g(); 
         }catch(NullPointerException ex){
             //方式1
             Exception e=new Exception();
             //將原始的異常信息保留下來
             e.initCause(ex);
             //方式2
             //Exception e=new Exception(ex);
             try {
    		    throw e;
    		} catch (Exception e1) {
    		    e1.printStackTrace();
    		}
         }
    }

    public void g() throws NullPointerException{
    	System.out.println("inside g()");
        throw new NullPointerException();
    }
}

public class RunMain {
    public static void main(String[] agrs) {
    	try{
            new ReThrow().f();
        }
        catch(Exception e){
            System.out.println("inside main()");
            e.printStackTrace(System.out);
        }
    }
}
複製代碼

在這個例子裏面,咱們先捕獲NullPointerException異常,而後在拋出Exception異常,這時候若是咱們不使用initCause方法將原始異常(NullPointerException)保存下來的話,就會丟失NullPointerException。只會顯示Eception異常。下面咱們來看結果:

//沒有調用initCause方法的輸出
inside g()
java.lang.Exception
	at com.learn.example.ReThrow.f(RunMain.java:9)
	at com.learn.example.RunMain.main(RunMain.java:31)
//調用initCasue方法保存原始異常信息的輸出
inside g()
java.lang.Exception
	at com.learn.example.ReThrow.f(RunMain.java:9)
	at com.learn.example.RunMain.main(RunMain.java:31)
Caused by: java.lang.NullPointerException
	at com.learn.example.ReThrow.g(RunMain.java:24)
	at com.learn.example.ReThrow.f(RunMain.java:6)
	... 1 more
複製代碼

咱們看到咱們使用initCause方法保存後,原始的異常信息會以Caused by的形式輸出。

Java異常的限制

當Java異常遇到繼承或者接口的時候是存在限制的,下面咱們來看看有哪些限制。

  • 規則一:子類在重寫父類拋出異常的方法時,要麼不拋出異常,要麼拋出與父類方法相同的異常或該異常的子類。若是被重寫的父類方法只拋出受檢異常,則子類重寫的方法能夠拋出非受檢異常。例如,父類方法拋出了一個受檢異常IOException,重寫該方法時不能拋出Exception,對於受檢異常而言,只能拋出IOException及其子類異常,也能夠拋出非受檢異常。 咱們經過例子來看下:
class A {  
    public void fun() throws Exception {}  
}  
class B extends A {  
    public void fun() throws IOException, RuntimeException {}  
}
複製代碼

父類拋出的異常包含全部異常,上面的寫法正確。

class A {  
    public void fun() throws RuntimeException {}  
}  
class B extends A {  
    public void fun() throws IOException, RuntimeException {}  
}
複製代碼

子類IOException超出了父類的異常範疇,上面的寫法錯誤。

class A {  
    public void fun() throws IOException {}  
}  
class B extends A {  
    public void fun() throws IOException, RuntimeException, ArithmeticException{}
}
複製代碼

RuntimeException不屬於IO的範疇,而且超出了父類的異常範疇。可是RuntimeException和ArithmeticException屬於運行時異常,子類重寫的方法能夠拋出任何運行時異常。因此上面的寫法正確。

  • 規則兒:子類在重寫父類拋出異常的方法時,若是實現了有相同方法簽名的接口且接口中的該方法也有異常聲明,則子類重寫的方法要麼不拋出異常,要麼拋出父類中被重寫方法聲明異常與接口中被實現方法聲明異常的交集。
class Test {
    public Test() throws IOException {}
    void test() throws IOException {}
}

interface I1{
    void test() throw Exception;
}

class SubTest extends Test implements I1 {
    public SubTest() throws Exception,NullPointerException, NoSuchMethodException {}
    void test() throws IOException {}
}
複製代碼

在SubTest類中,test方法要麼不拋出異常,要麼拋出IOException或其子類(例如,InterruptedIOException)。

Java異常與構造器

若是一個構造器中就發生異常了,那咱們如何處理才能正確的清呢?也許你會說使用finally啊,它不是必定會執行的嗎?這可不必定,若是構造器在其執行過程當中遇到了異常,這時候對象的某些部分尚未正確的初始化,而這時候卻會在finally中對其進行清理,顯然這樣會出問題的。
原則:
對於在構造器階段可能會拋出異常,而且要求清理的類,最安全的方式是使用嵌套的try子句。

try {
    InputFile in=new InpputFile("Cleanup.java");
    try {
    	String string;
    	int i=1;
    	while ((string=in.getLine())!=null) {}
    }catch (Exception e) {
    	System.out.println("Cause Exception in main");
    	e.printStackTrace(System.out);
    }finally {
    	in.dispose();
    }
}catch (Exception e) {
    System.out.println("InputFile construction failed");
}
複製代碼

咱們來仔細看一下這裏面的邏輯,對InputFile的構造在第一個try塊中是有效的,若是構造器失敗,拋出異常,那麼會被最外層的catch捕獲到,這時候InputFile對象的dispose方法是不須要執行的。若是構形成功,那麼進入第二層try塊,這時候finally塊確定是須要被調用的(對象須要dispose)。

異常的使用指南(下列狀況下使用異常)

  • 在恰當的級別處理異常(在知道如何處理的狀況下才捕獲異常)
  • 努力解決問題而且從新調用產生異常的方法
  • 進行少量修補,而後繞過異常的地方從新執行
  • 把當前運行環境下能作的事情儘可能作完,而後把相同的異常重拋到更高層
  • 把當前運行環境下能作的事情儘可能作完,而後把不相同的異常重拋到更高層
  • 努力讓類庫和程序更安全
相關文章
相關標籤/搜索