Java——異常

前言

Java中使用異常機制去處理程序錯誤,減小了錯誤處理代碼的複雜度。沒必要在程序每一個可能出現錯誤的地方都進行檢查並添加錯誤處理代碼,從而顯得程序主要結構混亂。異常機制會捕獲錯誤,而且在異常處理程序中處理錯誤,使得程序代碼和錯誤處理代碼分離,使得代碼結構更清晰明瞭。下面將介紹Java中的異常分類、如何建立異常處理程序以及自定義異常等。java

異常分類


Java中的全部異常都繼承自Throwable。程序員

Throwable:被用來表示任何能夠做爲異常被拋出的類。有兩個重要的子類Exception和Eerror。兩者都是Java異常處理的重要子類,而且兩者也包含許多重要的子類。編程

Error該類層次結構描述了Java運行時系統內部錯誤和資源耗盡錯誤,總之是與Java虛擬機有關的運行錯誤。也是應用程序沒法處理的。這些錯誤是不可查的,由於它們在應用程序的控制和處理能力之 外,並且絕大多數是程序運行時不容許出現的情況。對於設計合理的應用程序來講,即便確實發生了錯誤,本質上也不該該試圖去處理它所引發的異常情況。在 Java中,錯誤經過Error的子類描述。通常不須要你關心。數組

Exception: 是程序自己能夠處理的異常,是須要程序員關心的異常類。網絡

Java中的異常又可分爲可檢查異常(checked exceptions)和不可檢查異常(unchecked exceptions)。函數

  • 可檢查異常:編譯器要求必須處理的異常。在Exception類及其子類中,除了RuntimeException類及其子類外,其他都是可檢查異常。這種異常編譯器會強制要求處理它,須要使用try-catch語句去捕獲處理或者使用trhrow子句拋出該異常讓其餘地方去處理它,不然編譯器會不容許編譯經過。
  • 不可檢查異常:編譯器不強制要求處理的異常。該異常分來包括:運行時異常(RuntimeException)及其子類和錯誤(Error)及其子類。這種異常即便不使用try-catch子句捕獲和throw子句拋出,編譯器也會使得編譯經過。

Exception異常只有RuntimeException及其子類是不可查的異常,其他異常都是可查異常。因而,Excepption異常分類就能夠分爲運行時異常非運行時異常(編譯時異常)ui

運行時異常如NullPointerException(空指針異常)、ArrayIndexOutOfBoundException(數組下標越界異常)編譯器不強制要求處理 。這些異常通常是因爲程序邏輯錯誤引發,程序應該從邏輯角度避免這種異常的發生。非運行時異常則須要強制要求處理,不然編譯不予經過。.net

下面再附上一張比較完整的Java異常類圖:
線程

異常處理

Java程序中的異常處理機制爲:拋出異常,捕獲異常設計

拋出異常

異常情形(exceptionanl condiition)是指阻止當前方法或做用域繼續執行的問題。須要與普通問題區分開:普通問題指的是在當前環境下可以獲得足夠的信息,總可以處理這個問題;異常情形就是,程序不能執行下去了,在當前環境下沒法獲必要的信息來解決問題。須要作的就是從當前環境中跳出,並把問題交給上一級環境讓它去處理。這就是拋出異常所發生的事情。

例如:除法問題。如果事先沒有對除數爲0進行判斷,那麼碰見除數爲0時,就是一個異常狀況。固然的環境不知道如何處理這個狀況,因而就將異常拋出。

Java中拋出異常的實現方式:

  • 使用new在堆上建立異常對象。
  • 當前的執行路徑被終止,並從當期環境中使用throw關鍵字彈出異常對象的引用
  • 異常處理機制接管程序,並開始尋找一個恰當的地方來繼續執行程序。恰當的地方就是指異常處理程序
  • 異常處理程序將程序從錯誤狀態中恢復,以使程序能要麼換一種方式運行,那麼繼續原來的程序執行。

舉一個例子:一個引用t爲null時,建立一個異常從當前環境中拋出,把錯誤傳播到更大的環境中去處理。這個例子只爲舉例說明,實際不這樣用。

if(t == null){
    throw new NullPointerException();
}

異常容許咱們強制程序中止運行,並告訴咱們出現了什麼問題,或者(理想情況下)強制程序處理問題,並返回到穩定狀態。

異常參數

與使用Java中其餘對象同樣,咱們使用new在對上建立異常對象,這也伴隨着存儲空間的分配和構造器的調用。全部標準異常都有兩個構造器:一個默認構造器和一個以字符串做爲參數的構造器。帶參構造器能夠將一些信息放入構造器中。

throw new NullPionterException(" t = null");

將這個字符串信息提取出來的方式有許多種,稍後介紹。

對於不一樣類型的錯誤,要拋出相應的異常。錯誤信息能夠保存在異常對象的內部或者使用異常類的名稱來暗示。上一層環境經過這些異常信息來決定如何處理這些異常。一般,異常對象中僅有的信息就是異常類型,除此以外不包含任何有意義的內容。

捕獲異常try-catch

要理解異常是如何被捕獲的,須要先理解監控區域(guarded region)的概念。它是一段可能產生異常的代碼,而且後面跟着處理這些異常的代碼。

try塊

若是在方法內部拋出了異常(或者該方法內部調用的其餘方法拋出了異常),那麼這個方法將在拋出異常的過程當中結束,方法中後續代碼將不能運行。若是不但願剛發就此結束,能夠在方法內部設置一個特殊的塊來捕獲異常。由於在這個快裏「嘗試」各類(可能產生異常的)方法調用,因此稱爲try塊。它是跟在try關鍵字後的普通代碼塊:

try{
    //可能會拋出異常的代碼
}

對於不支持異常處理的程序語言,想要仔細檢查錯誤,就須要在每一個方法調用的先後設置錯誤檢查代碼。可是有了異常處理機制,就能夠把全部動做均可以放在tty塊中,而後只須要在一個地方就捕獲全部異常。這也是前面說的,異常處理機制將完成任務的代碼和錯誤檢查的代碼分離,使得程序結構清晰易於閱讀。

異常處理程序

拋出的異常必須在某個地方獲得處理,這個地方就是咱們前面說的異常處理程序。針對每一個要捕獲的異常,須要準備相應的處理程序。異常程序緊跟在try塊以後,以關鍵字catch表示。

try{
    //可能會拋出異常的代碼
}catch(ExceptionType1 id1){
    //處理類型爲ExceptionType1異常的代碼
}catch(ExceptionType2 id2){
    //處理類型爲ExceptionType2異常的代碼
}

catch子句就是異常處理程序,看起來像是接收指定的異常類型參數的方法。異常處理程序必須緊跟在try塊以後。當異常被拋出時,異常處理機制將負責搜尋參數與拋出異常類相匹配的第一個處理程序。而後進入catch子句執行,此時就認爲異常獲得了處理。一旦catch子句結束,則處理程序的查找過程結束。

在try塊內部,許多不一樣的方法調用可能會產生相同類型的異常,而你只須要提供一個針對此類型的異常處理程序。

異常處理的兩種模型:終止模型和恢復模型

異常處理週期理論上有兩種模型:終止模型和恢復模型。Java支持終止模型(Java、C++等大多數語言支持的模型)。在這種模型中,將假設錯誤很是關鍵,以致於程序沒法返回到異常常常發生的地方繼續執行。一旦異常被拋出,就代表錯誤已經沒法挽回,也不能夠繼續回來執行。恢復模型則指異常處理程序的工做是修正錯誤,而後從新嘗試調用出問題的方法,並認爲第二次能夠成功。對於恢復模型,一般但願異常被處理以後能繼續執行程序。

雖然恢復模型很吸引人,可是不是很實用。其中主要的緣由多是它所致使的耦合:恢復性的處理程序須要瞭解異常拋出的地點,這勢必要包含依賴於拋出位置的非通用性代碼。這增長了代碼編寫和維護的困難。

若是想要使Java實現相似恢復的行爲,那麼在碰見錯誤的時候就不能拋出異常,而是調用方法來修正該錯誤。或者,把try-catch塊放在while循環裏,這樣就能夠不斷地進入try塊,直到獲得滿意的結果真後退出。例如:

boolean exit = false;
while(!exit){
    try{
        //編寫可能會出異常的代碼或者調用可能會出異常的方法
        f();
        //...  
        boolean = true;
    }catch(ExceptionType e){//捕獲相應類型異常進行處理
        //異常處理
    }
}

建立自定義異常

Java提供提供的異常體系可能不會徹底包含咱們碰見的錯誤,因此容許咱們能夠自定義異常。本身要自定義異常必須已知的異常繼承,最好是選擇意思相近的異常類繼承。創建新的異常最簡單的方法就是讓編譯器爲你產生默認的構造器,能夠較少寫的代碼量。

//自定義異常繼承自Exception
class SimpleException extends Exception{}

public class InheritingException {
    //thorws關鍵字說明該方法會產生SimpleException異常
    public void f() throws SimpleException{
        System.out.println("從f()中拋出SimpleException異常");
        throw new SimpleException();  //使用thorw關鍵字拋出SimpleException異常
    }
    
    public static void main(String[] args) {
        InheritingException ie = new InheritingException();
        try {
            //調用可能會拋出異常的方法
            ie.f();
        }catch(SimpleException e) {
            System.out.println("捕獲了SimpleException異常");
//          System.err.println("捕獲了SimpleException異常");
        }
    }
}
/*
output:
從f()中拋出SimpleException異常
捕獲了SimpleException異常
*/

編譯器建立了默認構造器,它將自動調用基類的默認構造器。能夠將錯誤信息發送到標準錯誤流,這樣更能引發用戶注意。

也能夠爲異常類定義一個接受字符串參數的構造器:

class MyException extends Exception{
    public MyException() {}
    //增長含參構造器
    public MyException(String msg) {
        super(msg);
    }
}

public class FullConstructors {
    public static void f() throws MyException{
        System.out.println("從f()中拋出異常");
        throw new MyException();
    }
    
    public static void g() throws MyException{
        System.out.println("從g()中拋出異常");
        throw new MyException("從g()中產生");
    }
    
    public static void main(String[] args) {
        try {
            f();
        }catch(MyException e) {
            e.printStackTrace(System.out);
        }
        
        try {
            g();
        }catch(MyException e) {
            e.printStackTrace(System.out);
        }
    }
}
/*
output:
從f()中拋出異常
blogTest.MyException
    at blogTest.FullConstructors.f(FullConstructors.java:17)
    at blogTest.FullConstructors.main(FullConstructors.java:27)
從g()中拋出異常
blogTest.MyException: 從g()中產生
    at blogTest.FullConstructors.g(FullConstructors.java:22)
    at blogTest.FullConstructors.main(FullConstructors.java:33)
*/

兩個構造器定義了建立MyException類對象的建立方式。對於第二個構造器,使用super關鍵字明確調用了其基類構造器,它接受一個字符串做爲參數。

在異常處理程序中,調用了在Throwable類中聲明的printStackTrace()方法,它將打印「從方法調用處直到異常拋出處」的方法調用序列。這裏,信息將被髮送到System.out中,並自動地被捕獲和顯示在輸出中。如果調用printStackTrace()原始版本

e.printStackTrace();

則信息將被輸出到標準錯誤流。

異常說明

Java提供了相應語法,是你能夠告知客戶端程序員某個方法可能會拋出什麼異常,而後客戶端程序員就能夠對可能出現的異常進行處理。這就是異常說明,屬於方法聲明的一部分,緊跟在形式參數列表以後。就如上代碼中:

//thorws關鍵字說明該方法會產生SimpleException異常
    public void f() throws SimpleException{
        System.out.println("從f()中拋出SimpleException異常");
        throw new SimpleException();  
    }

代碼必須和異常說明一致。 如果方法中產生了異常(拋出異常),那麼編譯器就會發現並提醒你:要麼在這個方法中處理這個異常,要麼在異常說明裏代表此方法會產生該異常。

咱們須要注意:雖然產生異常若是不處理就必需要進行說明,可是咱們卻能夠說明異常但實際不拋出。編譯器相信了這個聲明,並強制此方法的用戶像真的拋出這種異常那樣使用這個方法。這樣作的好處在於:爲異常先佔一個位置,之後就能夠拋出這種異常而不用修改已有的代碼。在定義抽象類基類和接口時這種能力特別重要,這樣派生類或接口實現就可以拋出這些預先聲明的異常

Exception異常及一些異常類的方法

捕獲全部異常

若要只寫一個異常捕獲全部類型的異常,那就經過捕獲異常類型的基類Exception實現。(實際上有其餘基類,可是Exception是同編程活動相關的基類。)

catch(Exception e){
    // 異常處理代碼
}

上面這段代碼將捕獲全部異常,作好將其放處處理程序列表末尾,以防止它搶在其餘處理程序之間先將異常捕獲。由於基類異常能夠捕獲子類異常。

從API文檔能夠看出異常基類Exception沒有含有太多的具體信息,從文檔也能夠它的基類Throwable含了很多方法。


Exception類自身沒有含有任何方法


Throwable類所含方法

因此咱們能夠利用從Throwable基類繼承來到方法:String getMessage()、String getLocalizedMessage()來獲取詳細信息或用本地語言表示的詳細信息。
String toString()返回對Throwable的簡單描述。
void printStackTrace()、void printStackTrace(PrintStream s)、 void printStackTrace(PrintWriter s)打印Throwable和Throwable的調用軌跡。調用棧顯示了「把你帶到異常拋出地點」的方法調用序列。第一個版本輸出到標準錯誤,後兩個版本容許選擇要輸出的流。
Throwable fillInStackTrace()用於在Throwable對象的內部記錄棧幀的當前狀態。這在程序從新拋出錯誤或異常時頗有用。

棧軌跡

printStackTrace()方法所提供的信息能夠經過StackTraceElement[] getStackTrace()方法來直接訪問。這個方法返回一個由棧軌跡中的元素所構成的數組,其中的每個元素都表示棧中的一幀。元素0是棧頂元素,而且是調用序列的最後一個方法調用(即這個Throwable被建立和拋出之處)。數組最後一個元素即棧底元素是調用序列中的第一個方法調用。舉一小例:

public class WhoCalled {
    public static void f() {
        try {
            throw new Exception();
        }catch(Exception e) {
            for(StackTraceElement ste : e.getStackTrace()) {
                System.out.println(ste.getMethodName());    //打印方法名
            }
        }
    }
    public static void g() {f();}
    
    public static void main(String[] args) {
        f();
        System.out.println("-------------");
        g();
    }
}
/*
output:
f
main
-------------
f
g
main
*/

從新拋出異常

從新拋出異常會把異常給上一級環境中的異常處理程序,同一個try塊後面的catch子句將被忽略。被拋出的異常的信息要保持,這樣上一級的異常處理程序才能夠獲得該異常的全部信息。若只是將異常簡單從新拋出那麼printStackTrace()顯示的將仍然是原來異常拋出點的調用棧信息,不是當前拋出點的信息如果想要更新信息,能夠調用fillInStackTrace()方法,這個方法將返回一個Throwable對象,它是經過把當前調用棧信息填入原來那個異常對象而創建的


從樣例輸出能夠看出單純拋出異常,調用棧信息確實沒有改變。使用fillInStack()方法,異常的發生地就改變了。

有可能在捕獲異常以後拋出另一種異常,那麼有關原來異常發生點的信息就會丟失,剩下的全是與與新拋出點有關的信息。前一個異常對象由於是在堆上面建立的,因此垃圾回收器會自動將它們清理掉,沒必要擔憂。

異常鏈

如果想在捕獲一個異常以後拋出新的異常,而且但願把原來的異常信息保存下來,這就是異常鏈。如何實現呢?

Throwable的子類構造器中能夠接受一個cause(因由)對象做爲參數。這個cause就用來表示原始異常,因而這樣就能夠把原始異常傳遞給新的異常,使用新的異常就能夠跟蹤到原始異常。可是,在Throwable的子類中只有三種基本異常類提供了帶cause參數的構造器,它們是Error、Exception以及RuntimeException。如果把其餘類型異常連接起來,就應該使用initCasue()方法而不是構造器。

使用finally進行清理

對於一些代碼,咱們但願不管try塊當中的異常是否拋出,這些代碼均可以獲得執行。這一般適用於內存回收以外的狀況(內存回收是Java虛擬機完成),好比當異常拋出時關閉打開的文件資源。爲了達到這個效果,能夠在異常處理程序後面加上finally子句。

try-catch-finally

因此完整的異常處理程序看起來像是這樣:

try{
    //可能會拋出異常的代碼
}catch(ExceptionType1 id1){
    //處理類型爲ExceptionType1異常的代碼
}catch(ExceptionType2 id2){
    //處理類型爲ExceptionType2異常的代碼
}finally{
    //不管異常是否發生均可以被執行
}
public class FianllyTest {
    public static void main(String[] args) {
        try {
            throw new Exception();
        }catch(Exception e) {
            System.out.println("捕獲Exception異常");
        }finally {
            System.out.println("當異常發生時,finally子句1");
        }
        
        try {
            int i = 0;
        }catch(Exception e) {
            System.out.println("捕獲Exception異常");
        }finally {
            System.out.println("當異常沒有發生時,finally子句2");
        }
    }
}
/*
output:
捕獲Exception異常
當異常發生時,finally子句1
當異常沒有發生時,finally子句2
*/

從輸出可看出不管異常是否被拋出,finally子句老是可以被執行。

finally的用處

對於沒有垃圾回收機制和析構函數自動調用機制的程序語言來講,finally很是重要。它能使程序員保證:不管try塊裏面發生什麼,內存總能獲得釋放。可是Java擁有垃圾回收機制,內存釋放再也不是問題。那麼,Java在什麼狀況下使用finally呢?當要把除內存以外的資源恢復到它們的初始狀態時,就須要用到finally子句。這種須要清理的資源包括:已經打開的文件或者網絡鏈接,在屏幕上顯示到圖形,甚至是外部世界的某個開關等。

在return中使用finally

由於finally子句總會被執行,因此在一個方法中能夠從多個點返回,而且能夠保證重要的清理工做仍舊會執行。

注意:異常丟失

異常做爲程序出錯的標誌,決不該該被忽略,可是仍是難免會被忽視掉。好比使用特殊的方式使用finally子句,就會致使異常丟失:

由於try塊後面能夠直接跟finally塊。因此能夠致使在VeryImportantException異常尚未處理的狀況下就拋出另一個異常,致使這個異常丟失。若是要避免這種狀況就須要在每一個異常拋出後都須要緊跟異常處理程序。

還有一種更簡單的異常丟失方法是從finally子句中直接返回:

public class ExceptionSilencer{
    public static void main(String args[]){
        try{
            throw new RuntimeException();
        }finally{
            return;
        }
    }
}

這個程序即便拋出了異常,也不會有任何輸出。因此要注意這些特殊的finally的使用方法!

finally子句不會被執行的狀況

  • 在finally子句中產生了異常

  • 在前面的代碼中用了System.exit()退出程序

  • 程序所在的線程死亡

  • 關閉CPU

小結

介紹了Java中異常類的分類,按異常發生類型分和按可檢查和不可檢查分。其次介紹了Java中如何拋出一個異常以及如何捕獲一個異常,即try-catch語句塊的使用。如何自定義一個異常,咱們須要注意Java中的異常類已經涵蓋了大部分會出現的異常,除非特殊狀況,通常不須要去自定義異常。throws異常聲明的使用。而後介紹異常類中的經常使用方法(用藍色標記的方法),幾乎都是繼承自Throwable類。最後介紹了finally子句,要注意使用finally,避免出現異常丟失!

參考:

Java異常繼承關係:http://www.benchresources.net/exception-hierarchy-in-java/

深刻理解java異常處理機制: http://www.javashuo.com/article/p-sulhotdq-cr.html

《Java編程思想》第四版

相關文章
相關標籤/搜索