java異常那些事

爲何須要異常機制:

Java的基本理念是「結構不佳的代碼不能運行」 --- Java編程思想

最理想的是在編譯時期就發現錯誤,但一些錯誤要在運行時纔會暴露出來。對於這些錯誤咱們固然不能置之不理。對於錯誤而言的兩個關鍵是發現和處理錯誤。Java提供了統一的異常機制來發現和處理錯誤。java

不考慮異常存在來看一下這個場景:編程

public void showObject(Object obj) {
    if (obj == null) {
        System.out.println("error obj is null");
    } else {
        System.out.println(obj.toString());
    }
}

對於showObject來講obj爲null是一個錯誤,要在輸出以前作錯誤判斷,發生錯誤的話把錯誤打印出來做爲錯誤報告和處理。這裏把錯誤的發現、報告處理和正常業務邏輯放在了一塊兒。但一些錯誤每每比這複雜且不僅一個,若是咱們爲每個錯誤都去定義一個獨特錯誤報告的形式且都將錯誤處理代碼和正常業務代碼牢牢的耦合在一塊兒那咱們代碼會變得難以維護。數組

合理的使用異常不只能使咱們的代碼更加健壯,還能簡化開發提高開發效率。spa

1、拋出異常(發現錯誤):

一、異常也是對象:

java使用使用異常機制來報告錯誤。異常也是普通的類類型。Java自身已經定義好了使用java時可能會產生的異常,使用java時java會自動去檢查異常的發生,當異常發生時,java會自動建立異常對象的實例並將其拋出。咱們常常看到的NullPointerException即是java已經定義好的異常。code

除了java自身定義的異常外咱們能夠自定義異常,但自定義的異常須要咱們本身去檢查異常情形的發生,並本身建立異常對象和拋出。固然也能夠建立java自定義的異常並拋出,拋出異常使用throw關鍵字:對象

throw new Exception();

咱們使用的第三方庫大多封裝了本身的異常,並在異常情形發生時將自定義異常經過throw拋出。全部的異常類型都繼承自Throwable類,全部Throwable類型的對象均可被拋出。繼承

二、拋出異常:

異常發生,系統自動建立異常實例並拋出,或咱們本身建立異常實例拋出異常時,代碼的正常執行流程將會被終止,轉而去執行異常處理代碼。內存

2、異常捕獲(處理錯誤):

一、監控區域:

當異常拋出時,天然拋出的異常應該獲得處理,這就須要將拋出的捕獲異常。但一個異常類型可能在不少地方被拋出,那麼怎麼去對特定的地方編寫特定的異常處理程序那?java採用一個最方便和合理的方式,即對可能產生異常的代碼區域進行監控,並在該區域後添加處理程序。資源

監控的代碼區域放在try{}中,而異常處理代碼緊跟在try後的catch中:開發

try {
    /***/
} catch (ExceptionType e) {
    /*
    ***
    */
}

catch相似方法的申明括號中爲異常的類型和該類類型的實例。代表當前catch塊處理的是什麼類型的異常,而e即是該異常拋出的實例對象。

當try內的代碼拋出異常時,就會中止當前流程,去匹配第一個catch中申明的異常類型與拋出類型相同的catch,若是匹配到則執行其內代碼。一個try中可能會拋出多種類型的異常,能夠用多個catch去匹配。

二、捕獲全部異常:

注意catch中聲明的異常若是爲當前拋出異常的父類型也能夠匹配。因此通常將基類的異常類型放在後面。

由於全部能夠進行捕獲的異常都繼承自Exception,全部能夠catch中申明Exception類型的異常來捕獲全部異常,但最後將其放在最後防止將其餘異常攔截了。

3、從新拋出異常

1、異常的兩種類型

先看一下異常的類層次結構圖:
image

咱們能夠將異常分爲檢查和非檢查兩種類型:

  • 檢查異常:該類異常包含Exception及其子類異常,這些類型的異常的拋出必須有相應的catch進行捕獲,不然沒法經過編譯。
  • 非檢查異常:該類型的異常包含RuntimeException、Error和二者的子類型,這類異常能夠沒有對應的try-catch進行捕獲也可經過編譯。當異常發生時沒有相應的捕獲則異常會自動向上一級拋出,如此若是一直到main方法中還未被捕獲則會調用該異常的printStacjTrace方法輸出異常信息,並終止main的運行。其中Error爲Java自身錯誤,這類錯誤發生時咱們並不能在業務代碼中去解決,如內存不足,因此這類異常不須要去捕獲。
一、異常聲明:

catch中的語句執行完成後會繼續執行try-catch後的其餘語句。因此當try-catch後還有語句時,必定要保證但異常發生時在catch中已經對異常進行了正確處理,後面的代碼能夠獲得正常的運行,若是不能保證則應該終止代碼向後的執行或再次拋出異常。

一些異常在當前的方法中不須要或沒法進行處理時,能夠將其拋出到上一層。要在方法中將異常拋出須要在方法中對要拋出的異常進行聲明,這樣方法的調用者才能知道哪些異常可能會拋出,從而在調用方法時添加異常處理代碼。

非檢查異常拋出到上一級時能夠不用進行聲明,合理的使用非檢查異常能夠簡化代碼。

(1) 異常聲明

在方法聲明的參數列表以後使用throws進行異常聲明,多個異常類型使用逗號隔開:

void t () thrwos ExcptionTyep1, ExceptionType2 {
    
}

在方法中聲明瞭的異常在方法中能夠不進行捕獲,直接被拋出到上一級。異常聲明父類異常類型能夠匹配子類異常類型,這樣當有多個子類異常拋出時,只用聲明一個父類異常便可,子類異常將被自動轉換爲父類型。

4、建立自定義異常:

要建立本身的異常必須得繼承自其它的異常,通常繼承Exception建立檢測異常,繼承RumtimeException建立非檢查異常。

通常狀況下異常提供了默認構造器和一個接受String參數的構造器。對於通常自定義的異常來講,只須要實現這兩個構造方法就足夠了,由於定義異常來講最有意義的是異常的類型,即異常類的名字,但當異常發生時只需看到這個異常的類型就知道發生了什麼,而其餘一些操做在Throwable中已經有定義。因此除非有一些特殊操做,否則在自定義異常時只需只需簡單的實現構造方法便可。

5、異常信息:

因此異常的根類Throwable定義了咱們須要的大多數方法:

// 獲取建立異常時傳入的字符串
String getMessage()
// 使用System.err輸出異常發生的調用棧軌跡
void printStackTrace()
// 使用傳入的PrintStream打印異常調用棧
void printStackTrace(PrintStream s)
// 使用PrintStreamOrWriter打印異常調用棧
void printStackTrace(PrintStreamOrWriter s)

獲取調用棧實例

StackTraceElement[] getStackTrace()

該方法放回StackTraceElement數組,StackTraceElement爲調用方法棧的實例,改類型有如下經常使用方法:

// 返回棧代碼所在的文件名
String getFileName()
// 返回異常拋出地的行號
int getLineNumber()
// 返回棧的類名
String getClassName()
// 放回棧的方法名
String getMethodName()

6、異常鏈:

一、從新獲取異常

當咱們捕獲到一個異常時可能想將他在次拋出,但這樣直接拋出的話異常的棧信息是該異常原來的棧信息,不會是最新的再次拋出的異常的棧信息。以下:

class SimpleException extends Exception {
    public SimpleException() {
        
    }
    public SimpleException(String msg) {
        super(msg);
    }
}

public class Test {

    public void s() throws SimpleException {
        throw new SimpleException();
    }
    
    public void s2() throws SimpleException {
        try {
            s();
        } catch(SimpleException e) {
            throw e;
        }
    }
    
    public static void main(String[] args) {
        Test t = new Test();
        try {
            t.s2();
        } catch (SimpleException e) {
            e.printStackTrace();
        } 
    }
}

上面代碼輸出爲:

com.ly.test.javatest.exceptiontest.SimpleException
    at com.ly.test.javatest.exceptiontest.Test.s(Test.java:19)
    at com.ly.test.javatest.exceptiontest.Test.s2(Test.java:24)
    at com.ly.test.javatest.exceptiontest.Test.main(Test.java:33)

能夠看到異常拋出最終地爲 com.ly.test.javatest.exceptiontest.Test.s(Test.java:19),但若是咱們想讓異常拋出地變爲s2那?畢竟咱們在這裏本身拋出了異常。
Thrwoable類的fillInStackTrac建立一個新的Throwable對象,並將當前棧信息作新建立的Throwable異常的異常棧信息,而後返回。

二、異常鏈:

上面的作法又有另一個問題,若是咱們使用fillInStackTrace得到新的異常,那原來的異常信息也就丟失了,若是咱們想拋出新的異常當又得包含原來的異常那?

Error、Exception和RuntimeException都含有一個接受Throwable對象的構造方法,在建立新的異常時時傳入原來異常,便可保存原來異常。須要時使用getCause來獲取到。除了使用構造方法傳入異常,還可以使用initCase方法傳入異常。這其中的潛臺詞是「改異常是由什麼異常形成的」。以下:

public class Test {

    public void s() throws Exception {
        throw new Exception();
    }
    
    public void s2() throws Exception {
        try {
            s();
        } catch(Exception e) {
            Exception ne = (Exception)e.fillInStackTrace();
            ne.initCause(e);
            throw ne;
        }
    }
    
    public static void main(String[] args) {
        Test t = new Test();
        try {
            t.s2();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
}

6、老是執行的finally

看一下面的代碼:

public class Test {
    public static void s() throws IOException {
        throw new IOException();
    }
    public static void main(String[] args) {
        String fileName = "C:\\temp\\test.txt";
        File file = new File(fileName);
        InputStream in = null;
        
        try {
            in = new FileInputStream(file);
            s();
            int tempbyte = in.read();
            in.close();
        } catch (IOException e) {
            if (in != null) {
                System.out.println("in");
            }
            e.printStackTrace();
        }
    }
}

能夠看到要對in進行close但正常的流程中發生了異常,致使正常流程中的in.close沒法執行,便跳到cattch中去執行,上面因而又在catch中寫了一個關閉。着只是一個簡單清理操做,但若是須要執行的清理操做不止一行而是很是多那?也是在正常流程和catch中寫兩遍嗎,這樣是很是不友好的,因此java提供了finally,以下

public class Test {
    public static void s() throws IOException {
        throw new IOException();
    }
    public static void main(String[] args) {
        String fileName = "C:\\temp\\test.txt";
        File file = new File(fileName);
        InputStream in = null;
        
        try {
            in = new FileInputStream(file);
            s();
            int tempbyte = in.read();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                System.out.println("in");
            }
        }
    }
}

finally中的代碼不管異常是否發生都會被執行,即便try中包含return語句,也會在放回以前執行finally語句。

對於清理操做和一些異常發生也必獲得執行的代碼都應該放到finally中。

清理未建立的資源:

上面介紹使用finally來釋放資源,但看下面這個情形:

public void test() {
    try {
        in = new BufferedReader(new FileReader());
        String s = in.readLine();
    } catch (FileNotFoundException e) {
        
    } catch (Exception e) {
        try {
            in.close();
        } catch (IOException e2) {
            System.out.println("in class false");
        }
    } finally {
        //in.close();
    }
}

這個例子能夠看到若是new FileReader拋出了FileNotFoundException,那麼in是不會被建立的,若是此時還在finally中執行in.close()那麼天然是行不一樣的。但若是拋出了IOExceptin異常,那麼說明in成功建立但在readLine時發生錯,因此在catch中進行close時in確定已經被建立。這種情形資源的釋放應該放到catch中。

7、異常丟失:
一、在fianlly中return
public class Test {
    
    public static void main(String[] args) {
        try {
            int i = throwException();
            System.out.println(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static int throwException () throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            throw e;
        } finally {
            return 1;
        }
    }
}

上面代碼輸出:1

二、在finally中拋出異常
public class Test {
    
    public static void main(String[] args) {
        try {
            int i = throwException();
            System.out.println(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static int throwException () throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            throw e;
        } finally {
            throw new NullPointerException();
        }
    }
}

上面代碼輸出爲:

java.lang.NullPointerException
    at com.ly.test.javatest.Test.throwException(Test.java:20)
    at com.ly.test.javatest.Test.main(Test.java:7)

能夠看到main中捕獲到的是NullPointerException,首先拋出的Exception異常丟失了。

在開發中非特殊情形應避免以上兩種狀況的出現。

8、異常限制:

父類構造器中聲明的異常在基類的構造器中必須也聲明,由於父類的構造器老是會顯示會隱式(默認構造器)的被調用,而在子類構造器中是沒法捕獲父類異常的。但子類能夠添加父類中沒有聲明的異常。

重載方法時子類只可拋出父類中聲明的異常,由於咱們會將子類對象去替換基類,這時若是重載的方法添加類新的異常聲明,那麼原來的異常處理代碼將沒法再正常工做。但子類方法能夠減小或不拋出父類方法聲明的異常。

相關文章
相關標籤/搜索