Java—異常

Java異常指的是運行時出現(xiàn)的錯誤,異常發(fā)生時,是任程序自生自滅,立刻退出終止,還是輸出錯誤給用戶?或者用C語言風格:用函數(shù)返回值作為執(zhí)行狀態(tài)?。Java提供了更加優(yōu)秀的解決辦法:異常處理機制。
異常處理機制能讓程序在異常發(fā)生時,按照代碼的預先設定的異常處理邏輯,針對性地處理異常,讓程序盡最大可能恢復正常并繼續(xù)執(zhí)行,且保持代碼的清晰。
Java中的異常可以是函數(shù)中的語句執(zhí)行時引發(fā)的,也可以是程序員通過throw 語句手動拋出的,只要在Java程序中產(chǎn)生了異常,就會用一個對應類型的異常對象來封裝異常,JRE就會試圖尋找異常處理程序來處理異常。
Throwable類是Java異常類型的頂層父類,一個對象只有是 Throwable 類的(直接或者間接)實例,他才是一個異常對象,才能被異常處理機制識別。JDK中內(nèi)建了一些常用的異常類,我們也可以自定義異常。

Java異常的分類和類結(jié)構(gòu)圖

網(wǎng)絡圖片
Throwable又派生出Error類和Exception類。

錯誤:Error類以及他的子類的實例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理,Error很少出現(xiàn)。因此,程序員應該關(guān)注Exception為父類的分支下的各種異常類。

異常:Exception以及他的子類,代表程序運行時發(fā)送的各種不期望發(fā)生的事件??梢员籎ava異常處理機制使用,是異常處理的核心。

非檢查異常和檢查異常

非檢查異常(unckecked exception):Error 和 RuntimeException 以及他們的子類。javac在編譯時,不會提示和發(fā)現(xiàn)這樣的異常,不要求在程序處理這些異常。所以如果愿意,我們可以編寫代碼處理(使用try…catch…finally)這樣的異常,也可以不處理。對于這些異常,我們應該修正代碼,而不是去通過異常處理器處理 。這樣的異常發(fā)生的原因多半是代碼寫的有問題。如除0錯誤ArithmeticException,錯誤的強制類型轉(zhuǎn)換錯誤ClassCastException,數(shù)組索引越界ArrayIndexOutOfBoundsException,使用了空對象NullPointerException等等。

檢查異常(checked exception):除了Error 和 RuntimeException的其它異常(圖中的“非運行時異?!保?。javac強制要求程序員為這樣的異常做預備處理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch語句捕獲它并處理,要么用throws子句聲明拋出它,否則編譯不會通過。這樣的異常一般是由程序的運行環(huán)境導致的。因為程序可能被運行在各種未知的環(huán)境下,而程序員無法干預用戶如何使用他編寫的程序,于是程序員就應該為這樣的異常時刻準備著。如:SQLException,IOException,ClassNotFoundException 等。

例子:
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 ;
    }
}
//運行結(jié)果
----歡迎使用命令行除法計算器----
10 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
  at AllDemo.devide(AllDemo.java:19)
  at AllDemo.CMDCalculate(AllDemo.java:14)
  at AllDemo.main(AllDemo.java:7)

由打印結(jié)果分析:
第一行:
錯誤發(fā)生在“main” 線程中,錯誤類型:ArithmeticException,錯誤原因:被零除
第二、三、四行:
devide(),CMDCalculate(),main()三個函數(shù)是層級調(diào)用,形成調(diào)用棧的,只要一個函數(shù)發(fā)生了異常,那么他的所有的caller都會被異常影響。當這些被影響的函數(shù)以異常信息輸出時,就形成的了異常追蹤棧。異常最先發(fā)生的地方,叫做異常拋出點。

從上面的例子可以看出,當devide函數(shù)發(fā)生除0異常時,devide函數(shù)將拋出ArithmeticException異常,因此調(diào)用他的CMDCalculate函數(shù)也無法正常完成,因此也發(fā)送異常,而CMDCalculate的caller——main 因為CMDCalculate拋出異常,也發(fā)生了異常,這樣一直向調(diào)用棧的棧底回溯。這種行為叫做異常的冒泡,異常的冒泡是為了在當前發(fā)生異常的函數(shù)或者這個函數(shù)的caller中找到最近的異常處理程序。由于這個例子中沒有使用任何異常處理機制,因此異常最終由main函數(shù)拋給JRE,導致程序終止。

上面的代碼不使用異常處理機制,也可以順利編譯,因為2個異常都是非檢查異常。但是下面的例子就必須使用異常處理機制,因為異常是檢查異常。

例子:
import java.io.*;

public class ExceptionDemos {
    public static void main(String[] args) {
        new ExceptionDemos().testException();
    }

    public void testException() {
        FileInputStream fis = new FileInputStream("/Users/lincoln/a.txt");

        int word;

        while((word = fis.read()) != -1){
            System.out.println((char)word);
        }

        fis.close();
    }
}
//編譯報錯:
ExceptionDemos.java:9: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
        FileInputStream fis = new FileInputStream("/Users/lincoln/a.txt");
                              ^
ExceptionDemos.java:13: error: unreported exception IOException; must be caught or declared to be thrown
        while((word = fis.read()) != -1){
                              ^
ExceptionDemos.java:17: error: unreported exception IOException; must be caught or declared to be thrown
        fis.close();

由上例可知:IOException, FileNotFoundException是檢查異常,下面我們用一種異常處理方式使程序通過編譯。

例子:
import java.io.*;

public class ExceptionDemos {
    public static void main(String[] args) throws Exception {
        new ExceptionDemos().testException();
    }

    public void testException() throws IOException{
        FileInputStream fis = new FileInputStream("/Users/lincoln/a.txt");

        throw new IOException("手動拋出異常");
        int word;

        while((word = fis.read()) != -1){
            System.out.println((char)word);
        }

        fis.close();
    }
}

與上例不同的是我們在testException()方法中手動throw了IOException, 在testException()方法聲明了該方法可能會拋出IOException交給caller函數(shù)(而caller函數(shù)又把異常拋出)處理程序通過了編譯,說明IOException是 FileNotFoundException的子類。

throws 函數(shù)聲明:

throws聲明:如果一個方法內(nèi)部的代碼會拋出檢查異常(checked exception),而方法自己又沒有完全處理掉,則javac保證你必須在方法的簽名上使用throws關(guān)鍵字聲明這些可能拋出的異常,否則編譯不通過。

throws是另一種處理異常的方式,它不同于try…catch…finally,throws僅僅是將函數(shù)中可能出現(xiàn)的異常向調(diào)用者聲明,而自己則不具體處理。

采取這種異常處理的原因可能是:方法本身不知道如何處理這樣的異常,或者說讓調(diào)用者處理更好,調(diào)用者需要為可能發(fā)生的異常負責。

throw 異常拋出語句:

程序員也可以通過throw語句手動顯式的拋出一個異常。throw語句的后面必須是一個異常對象。

throw 語句必須寫在函數(shù)中,執(zhí)行throw 語句的地方就是一個異常拋出點,它和由JRE自動形成的異常拋出點沒有任何差別。

異常處理的基本語法

在編寫代碼處理異常時,對于檢查異常,有2種不同的處理方式:使用try…catch…finally語句塊處理它?;蛘?,在函數(shù)簽名中使用throws 聲明交給函數(shù)調(diào)用者caller去解決。

try…catch…finally語句塊
try{
     //try塊中放可能發(fā)生異常的代碼。
     //如果執(zhí)行完try且不發(fā)生異常,則接著去執(zhí)行finally塊和finally后面的代碼(如果有的話)。
     //如果發(fā)生異常,則嘗試去匹配catch塊。catch代碼遵循異常子類在前父類在后
 
}catch(SQLException SQLexception){
    //每一個catch塊用于捕獲并處理一個特定的異常,或者這異常類型的子類。Java7中可以將多個異常聲明在一個catch中。
    //catch后面的括號定義了異常類型和異常參數(shù)。如果異常與之匹配且是最先匹配到的,則虛擬機將使用這個catch塊來處理異常。
    //在catch塊中可以使用這個塊的異常參數(shù)來獲取異常的相關(guān)信息。異常參數(shù)是這個catch塊中的局部變量,其它塊不能訪問。
    //如果當前try塊中發(fā)生的異常在后續(xù)的所有catch中都沒捕獲到,則先去執(zhí)行finally,然后到這個函數(shù)的外部caller中去匹配異常處理器。
    //如果try中沒有發(fā)生異常,則所有的catch塊將被忽略。
 
}catch(Exception exception){
    //...
}finally{
    
    //finally塊通常是可選的。
   //無論異常是否發(fā)生,異常是否匹配被處理,finally都會執(zhí)行。
   //一個try至少要有一個catch塊,否則, 至少要有1個finally塊。但是finally不是用來處理異常的,finally不會捕獲異常。
  //finally主要做一些清理工作,如流的關(guān)閉,數(shù)據(jù)庫連接的關(guān)閉等。 
}
注意:

1、try塊中的局部變量和catch塊中的局部變量(包括異常變量),以及finally中的局部變量,他們之間不可共享使用。

2、每一個catch塊用于處理一個異常。異常匹配是按照catch塊的順序從上往下尋找的,只有第一個匹配的catch會得到執(zhí)行。匹配時,不僅運行精確匹配,也支持父類匹配,因此,如果同一個try塊下的多個catch異常類型有父子關(guān)系,應該將子類異常放在前面,父類異常放在后面,這樣保證每個catch塊都有存在的意義。

3、java中,異常處理的任務就是將執(zhí)行控制流從異常發(fā)生的地方轉(zhuǎn)移到能夠處理這種異常的地方去。也就是說:當一個函數(shù)的某條語句發(fā)生異常時,這條語句的后面的語句不會再執(zhí)行,它失去了焦點。執(zhí)行流跳轉(zhuǎn)到最近的匹配的異常處理catch代碼塊去執(zhí)行,異常被處理完后,執(zhí)行流會接著在“處理了這個異常的catch代碼塊”后面接著執(zhí)行。
有的編程語言當異常被處理后,控制流會恢復到異常拋出點接著執(zhí)行,這種策略叫做:resumption model of exception handling(恢復式異常處理模式 )
而Java則是讓執(zhí)行流恢復到處理了異常的catch塊后接著執(zhí)行,這種策略叫做:termination model of exception handling(終結(jié)式異常處理模式)

4、finally塊沒有處理異常的能力。處理異常的只能是catch塊。

5、在同一try…catch…finally塊中 ,如果try中拋出異常,且有匹配的catch塊,則先執(zhí)行catch塊,再執(zhí)行finally塊。如果沒有catch塊匹配,則先執(zhí)行finally,然后去外面的調(diào)用者中尋找合適的catch塊。

6、在同一try…catch…finally塊中 ,try發(fā)生異常,且匹配的catch塊中處理異常時也拋出異常,那么后面的finally也會執(zhí)行:首先執(zhí)行finally塊,然后去外圍調(diào)用者中尋找合適的catch塊。

自定義異常

如果要自定義異常類,則擴展Exception類即可,因此這樣的自定義異常都屬于檢查異常(checked exception)。如果要自定義非檢查異常,則擴展自RuntimeException。

異常的注意事項

1、當子類重寫父類的帶有 throws聲明的函數(shù)時,其throws聲明的異常必須在父類異常的可控范圍內(nèi)——用于處理父類的throws方法的異常處理器,必須也適用于子類的這個帶throws方法 。這是為了支持多態(tài)。
例如:父類方法拋出IOException, 子類方法可以拋出IOException,FileNotFoundException等。
試想:異常處理程序可以處理父類拋出的異常,如果父類引用指向子類對象,子類重寫父類的方法的拋出的異常不是父類拋出的異?;蛘咭膊皇瞧渥宇悾敲串惓L幚沓绦驘o法處理。

2、Java程序可以是多線程的。每一個線程都是一個獨立的執(zhí)行流,獨立的函數(shù)調(diào)用棧。如果程序只有一個線程,那么沒有被任何代碼處理的異常 會導致程序終止。如果是多線程的,那么沒有被任何代碼處理的異常僅僅會導致異常所在的線程結(jié)束。
也就是說,Java中的異常是線程獨立的,線程的問題應該由線程自己來解決,而不要委托到外部,也不會直接影響到其它線程的執(zhí)行。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容