一文給你通俗易懂的講解Java異常

什麼是異常?

最簡單的,看一個代碼示例:

public static void main(String[] args) {
        int a = 1;
        int b = 0;
        System.out.println(a / b);
    }

這段代碼有什麼問題?簡單,除數不能為0對吧,我們打印輸出:

顯而易見,程序出問題瞭,不能正常執行瞭,這裡出現瞭一些爆紅的信息,這些就是異常提示,這就是Java中提供的異常機制,當你的程序存在問題的情況下,會給你打印輸出一些信息,這個就叫做異常信息。

字面意思上去理解,所謂“異常”也就是“不正常”,放在代碼程序中就是那些導致不能正確執行的問題,比如上述代碼,Java就會給你打印出為啥此段代碼不能正確執行,給你輸出不正常的信息,這樣你就可以根據異常信息去修改代碼,從而提高代碼的健壯性!

詳細聊聊異常

以上我們簡單看瞭下一個具體的異常,下面我們就“何為異常”再直白的探討一下,異常作為一種代碼的處理機制,現在基本上大多數的編程語言都包含有這個異常機制,但是,我門熟知的偉大的C語言是沒有異常處理機制的。

大多數的高級語言,比如Java,python,C++這些都包含非常完善的異常處理機制,既然有這個玩意,那自然有它的好處,一般來說吧,擁有異常機制可以是我們的代碼:

  • 擁有更好的容錯性
  • 更加的健壯

那啥意思嘞?啥是容錯性,啥又是健壯呢?

首先是容錯性,這個通俗來講,就是可承受錯誤的范圍和概率,比如說我們的程序要是沒有異常機制的話,那很多錯誤是無法承受的,可能一旦出現錯誤,就會導致我們的系統崩潰出大問題,這個帶來的後果可能比較嚴重,但是具有異常機制,就可以幫助我們去處理一些錯誤,以至於即使出現錯誤也不會造成這麼嚴重的後果。

那什麼又是健壯呢?這個一般就是說我們的代碼比較安全,不容易出現bug,基本上把該想到的情況都想到瞭,代碼編寫比較嚴謹,不容易出錯,質量好,這個一般就可以說我們的代碼比較健壯。

當然,以上隻是我粗淺的理解,希望能夠幫助大傢對異常機制的理解。

那再來說異常,其實就是不好的東西,比如我們的代碼有bug,程序出錯等等,這些都是有可能發生的,誰也不能保證自己寫的代碼一定是正確的,對吧。

異常也就是代碼中可能出現的意外情況,這就要求我們在編寫代碼的時候盡量考慮全面,但是即使你考慮的再全面也不可能將所有的意外情況都考慮進去,所以,實際當中意外情況會有發生的概率,對於這種我們無法考慮周到的意外情況,就需要我們的異常機制去處理瞭。

Java中的異常

接下來我們來看看Java中的異常,想必大傢多多少少都會聽說過這樣一個異常叫做空指針異常,我們來看代碼演示:

NullPointerException nullPointerException = new NullPointerException("空指針異常");
        System.out.println(nullPointerException);

可以發現,在Java真實存在NullPointerException這個類,而我們可以通過這個類去創建具體的異常對象,比如這裡的空指針異常對象,我們打印輸出看看:

如此來看,在Java中,異常是以類的形式存在的,而且我們可以通過這些異常類去創建相應的異常對象,那麼我們再來看這段代碼:

public static void main(String[] args) {
        int a = 1;
        int b = 0;
        System.out.println(a / b);
    }

這裡會出現異常,其實實際上就是在運行到“ System.out.println(a / b);”的時候Jvm虛擬機就會在底層為我們創建出一個異常對象,從而將相關的異常信息打印輸出。

所以:

Java中異常是的的確確存在的類

Java的異常處理機制

接下來我們來說說Java的異常機制。我們還是來看上面那個代碼,也就是這個:

public static void main(String[] args) {
        int a = 1;
        int b = 0;
        System.out.println(a / b);
    }

這段代碼我們如果運行的話是會出錯的,也就是這樣:

這裡會給到我們一個異常信息,告訴我們說除數不能為0,然後程序就自動退出瞭,接下來我們再為這段代碼添加一個打印輸出:

很顯然這裡並不會執行後面的這句輸出語句,因為前面已經出現異常程序退出瞭,但是如果我們要求這裡的輸出必須執行該怎麼辦呢?

在Java中是提供瞭相對應的異常處理機制的,以上在a/b的時候出現瞭異常,在Java中我們是可以通過如下的方式去捕獲到這個異常的。

try-catch捕獲異常

具體的操作就是使用try- catch去捕獲我們的異常並作出相應處理,具體看代碼:

public static void main(String[] args) {

        int a = 1;
        int b = 0;
        try {
            System.out.println(a / b);
        } catch (Exception e) {
            System.out.println(e+":除數不能為0!");
        }
        
        System.out.println("執行到這裡……");
        
    }

我們在之前已經說過,在Java中,異常是以類的形式存在的,在我們寫的程序代碼中,隻要出現瞭異常,JVM就會給我們創建一個異常對象出來,這個時候,我們是需要對這個異常對象做處理的,如果你放任不管的話,最終導致的結果就是你的Java程序會退出。

所以啊,有異常你不處理,你的程序就會退出,那咋辦,處理啊,找到這個異常,處理它,那怎麼找到呢?

我們可以使用try去包裹可能出現的異常代碼,比如上述所講的代碼,在執行到a/b的時候可能出現異常,也就是b不能為0,這裡簡單說下,這裡的a和b是我們提前定義還好的,如果是讓用戶輸入a和b的值呢?

我們簡單改寫下代碼:

Scanner StringA = new Scanner(System.in);
        Scanner StringB = new Scanner(System.in);

        int a = Integer.parseInt(StringA.next());
        int b = Integer.parseInt(StringB.next());

        System.out.println(a/b);

        System.out.println("執行到這裡……");

這裡的意思是我們從鍵盤輸入去獲取這個a和b,那麼當我們輸入的是這樣的a和b的時候執行是沒什麼問題的:

可是一旦用戶不小心把b的值輸成0,那麼問題就來瞭:

所以這裡繞瞭一圈就是告訴大傢b/a這步操作是可能出現異常的,我們把這個操作叫做可能出現的異常代代碼塊,於是我們就可以使用try去操作這段代碼:

    try {
            System.out.println(a / b);
        } catch (Exception e) {
            System.out.println(e+":除數不能為0!");
        }

這裡要註意的就是,這個try和catch是一起配合使用的,catch是捕獲的意思,我們使用try包裹可能出現的異常代碼快,然後使用catch去捕獲這個異常對象,然後做出相應的處理,比如這裡,我們使用try包裹瞭a/b的操作,那麼當b不小心被賦值為零的時候,那麼這裡在運行的時候就會出現異常,由於在Java中異常是以類的形式存在,所以這裡會拋出一個異常對象。

那麼我們仔細看這個catch後面有個括號,就是異常對象參數,意思就是如果出現的這個異常對象屬於我括號裡的這個異常,那麼就進入這個catch塊去處理這個異常。

說白瞭就是,程序一旦出現異常,隨之而來就是會產生一個異常對象,而異常是以類的形式存在,那麼你就得為這個異常對象定義一個catch塊,這個異常對象會根據catch後的參數去找屬於自己的那一個catch塊,找到就進入該catch塊,沒有的話程序就有因為異常而終止瞭。

而除數為0這是一個叫做ArithmeticException的異常,也就是算術異常,而這個異常是繼承自Exception,也就是說Exception的范圍比ArithmeticException要大,所以Exception的catch塊可以處理身為子類的ArithmeticException異常。

異常類的繼承

以上我們說瞭ArithmeticException這個異常是繼承自Exception的,後者的范圍更廣,然後我們再看代碼:

        try {
            System.out.println(a / b);
        } catch (ArithmeticException e) {
            System.out.println(e + ":除數不能為0!出現算術異常");
        } catch (Exception e) {
            System.out.println(e+"出現異常");
        }

也就是說,一個try下面可以對應多個catch塊,而每一個catch都有自己對應的一個可以處理的異常類型,我們看上面的改進,我們添加瞭負責處理ArithmeticException的catch塊,那麼結果是如呢?

可以看到,這裡是直接進入瞭ArithmeticException的catch塊,也就是說,異常對象一旦被一個catch捕獲,就不會再進入下一個異常瞭。

有人可能會說可以這樣嗎?

也就是把Exception放在第一個catch塊,實際上這裡是不行的,為啥?我們來看下再這裡的一個異常繼承關系:

可以看到,ArithmeticException其實是Exception的子類的,如果你把Exception放在第一個catch塊的話那麼所以的異常對象都將直接被這個catch塊捕獲,因為所有的異常對象都是Exception或者其子類的實例對象,這就很關鍵啊,意味著你後面定義再多的catch塊也沒有用啊,因為永遠不會執行到這裡,上面已經被Exception這個老大哥給截胡瞭,在Java中永遠執行不到的代碼就會被定義為錯誤的,所以是不能把Exception給放到第一個catch塊的。

這裡有這麼一個原則:

先捕獲處理小范圍的異常,再捕獲處理大范圍的異常,也就是先小後大

意思也就是先把子類異常放在前面的catch塊,這麼以來,Exception的捕獲基本上都是在最後一個catch瞭。

多異常的處理

這個是在Java 7之後增加的,也就是說啊在Java7之前嘞,一般來說一個catch塊隻能捕獲處理一個異常,但是在Java7之後就升級瞭,可以一個catch塊捕獲處理多個異常。

那這個是怎麼操作的呢?來看看代碼就一目瞭然瞭:

是不是還是比較清楚的,這裡的編寫也很簡單,就是通過符號“|”把不同的異常對象類型給分隔開,記住這裡隻需要在最後定一個異常就行,也就是這裡的“e”,同時由於是捕獲多個異常,這裡的e其實是默認final修飾的,因此就不能再對e進行任何賦值操作瞭,比如一下這樣就是錯誤的:

這就是多個異常的捕獲瞭。

獲取異常信息

先明白這點:

當產生一個異常對象,被相對應的catch塊捕獲之後,這個catch塊後的異常形參變量也就接受到瞭這個異常對象。

因此,我們就可以通過這個異常參數去獲得一些異常信息,一般我們常用的一些方法如下:

  • getMessage():這個方法會返回異常的詳細描述信息
  • printStackTrace():這個方法會打印出異常的跟蹤棧信息
  • printStackTrace(PrintStream p):這個則會將異常的跟蹤棧信息輸出到指定的輸出流中去
  • getgtacktreace():返回異常的跟蹤站信息

那具體的訪問,我們看下代碼便知:

這裡其實也比較簡單,就是幾個常見的異常信息獲取方法的使用。

finally

這個可以說叫做善後的,啥意思嘞?簡單來說,就是你的異常對象無論進入哪個catch塊執行,那麼到最後這個finally裡的代碼一定會被執行。

這個一般用在哪裡呢?通常被用於釋放資源,一般比如說數據庫連接操作,網絡連接或者常見的IO流的操作,這些就需要進行資源的回收,那麼這個時候就可以使用finally裡,因為它必定會被執行。

看到這裡不知道大傢有沒有疑惑啊,不是說Java會自動回收資源嗎?這個感覺要手動操作啊,這裡其實你要區分資源的分類,Java的垃圾回收針對的堆內存中的對象所占用的內存,而這裡說的IO流操作,數據庫連接什麼都是屬於物理資源,而物理資源必須是需要手動回收的。

看看代碼:

這個時候我們看異常的處理就比較完整瞭,也就是包括try,然後是catch,再加上一定會被執行的finally塊。

那麼這裡就需要特別說一下瞭:

對於異常處理來說,try塊是必須的,沒有try塊啥也不是,而catch和finally則不是必須的,但是,也必須選擇其一,也就是說,你不能隻有個try,既沒有catch也沒有finally,然後就是註意catch塊瞭,可以有多個,但是要遵循“先小後大”的原則

接下來我們來看個測試,看代碼:

我們在這裡加入瞭return語句,一般來說吧,隻要程序的方法中碰到瞭return,那麼就會立即結束該方法,但是現在呢?我們看下結果:

這說明,finally語句一定會被執行!另外再給大傢說一個註意點:

如果你在finally中定義瞭return語句,那麼這個將導致你在try中定義的return語句失效,所以記住一點,不要在finally中使用return哦。

到這裡我們清楚瞭,對於finally語句來說是一定會被執行的(其實有例外,比如你調用瞭System.exit(1)退出虛擬機),我們常在finally中去做釋放資源的操作,但是你有沒有發現,這樣的操作覺得比較麻煩😡,那有沒有簡單的一些做法呢?

其實在Java7中對這個try語句進行瞭增強,可以讓我們不需要在finally中進行資源的關閉操作,可以自動幫我們關閉需要釋放的資源,但是這裡有個前提就是你所需要關閉的資源類要麼實現AutoCloseable接⼝,要麼實現Closeable接⼝,實際上在Java7中幾乎把所有的資源類都進行瞭改寫,主要就是都實現瞭AutoCloseable或者Closeable接⼝,可以讓其實現資源的自動關閉,這些資源類一般就是文件IO的各種類,或者是JDBC的Connection接口等等。

那到瞭Java9之後又對這個try語句進行瞭增強,在java7的改進中你需要在try後的圓括號內聲明並創建資源,到瞭Java9,你不需要這樣做瞭,隻需要自動關閉的資源有final修飾或者是有效的final即可,這裡先盡做瞭解,後期會詳細探討。

Checked異常和Runtime異常

接下來我們來看看關於異常的分類,Java中的異常可以分為兩個大類:

  • Checked異常
  • Runtime異常

那怎麼區分這兩類異常呢?所有的RuntimeException類及其⼦類的實例被稱為Runtime異常;不 是RuntimeException類及其⼦類的異常實例則被稱為Checked異常。

那對於Checked異常就是可檢查異常,也就是說在Java中認為這種異常是可以被提前處理的,所以一旦出現這種異常你就得處理它,如果不處理它,那是編譯都無法通過的。

那怎麼去處理這個Checked異常呢?我們前面也說瞭,可以使用try- catch的方式去捕獲處理異常,當然,我們還有一種方式就是拋出異常,暫且不管,這個等會會講。

對於Runtime異常也就是運行時異常瞭,這個我們不需要在編譯階段就處理它,如果要處理的話,可以使用try- catch,就比如上面我們一直演示的那個除數為0的案例。

throws

我們可以使用throws來聲明拋出異常,啥意思嘞,這個拋出異常咋回事?字面意思去理解,就是這個異常不管瞭,扔出去,對吧,拋出拋出,那如何扔出去呢?使用這個throws關鍵字即可。

也就是說當你不知道該如何處理某一類型的異常的時候,你就可以選擇將該異常拋出,實際上拋出異常也不是說就不管異常瞭,而是將該異常交給上一級調用者去處理。如果一直往上拋出異常,最終就把這個燙手山芋交給瞭JVM,那JVM是怎麼處理這個異常呢?

一般就是:

打印異常的跟蹤棧信息,並中止程序

下面我們來看下代碼:

我們這裡在main方法上使用throws拋出瞭這個異常,那就是把這個異常扔給瞭我們的JVM,而JVM的處理上面也說瞭,我們看下結果:

打印出跟蹤棧信息,然後中止程序,這裡其實是個運行時異常,也就是Runtime異常,接下來我們看下對於Checked異常的拋出,我們首先編寫一段含有Checked異常的代碼,如下:

這裡就會產生一個編譯時異常,那麼IDEA給我們的提示可以用try/catch捕獲處理,當然,也可以使用throws關鍵字拋出,我們這裡將其拋出:

接下來我們在main方法中去調用這個方法:

發現瞭嗎?我們在main方法中調用它依然是需要處理出現的異常的,本身CheckedTest將異常拋出,就是希望由調用者去處理該異常,所以這裡我們在main方法中去調用該方法的時候也要一並去處理該方法產生的異常,要不你繼續將其拋出交給JVM,要不使用try/catch捕獲!

Checked異常的限制

這裡給大傢看一個示例:

發現沒有,當我們使用throws去拋出一個異常時,父類中的方法拋出一個異常,而其子類中重寫該父類拋出異常的方法的時候,重寫後的方法拋出的異常的范圍是不能比父類中方法拋出的異常的范圍大的,這句話可能有點繞,但是配合看圖應該能明白什麼意思。

這其實就是Checked異常所帶來的一個限制。

手動拋出異常

以上我們使用throw是來拋出異常其實都是Java自動幫我們去拋出異常對象的,除此之外,我們還可以自己手動的去拋出異常,這裡需要使用到的一個關鍵字叫做throw,註意這裡是沒有s的,和以上我們說的throws是不一樣的。

想一下這裡為什麼要手動拋出異常呢?因為異常本身就不是確定的,什麼意思呢?就是同一件事情,在不同的人看來可能性質就不一樣,比如你明天要外出,可是明天突然就下雨瞭,那麼這個下雨對你來說就是一種異常,是你不想要的,但是對於那些尤為某種情況希望明天下雨的來說,這件事情就不是一件異常事件。

對應到我們的程序中,異常也是要根據具體情況來定義的,因此這種異常是系統無法幫我們來判定的,這就需要我們自行去拋出異常。

具體就是使用throw來手動拋出異常,怎麼操作的看代碼:

try {
            //規定第一次輸入的值不能大於10,也就是這裡的stringA不能大於10
            Scanner stringA = new Scanner(System.in);
            Scanner stringB = new Scanner(System.in);
            int a = Integer.parseInt(stringA.next());
            int b = Integer.parseInt(stringB.next());

            if (a > 10) {
                throw new Exception("輸入的第一個數字不能大於10");
            } else {
                System.out.println(a + b);
            }
        } catch (Exception e) {
    		System.out.println(e.getMessage());
            System.out.println("第一次輸入請輸入一個小於10的數字!");
        }

同樣的,當你手動的拋出一個異常的時候也是需要對這個異常進行處理的,我們這裡使用try/catch來捕獲處理該異常,看結果:

這裡說一點,就是無論你是手動拋出異常還是系統給我們拋出異常,在java中對異常的處理方式是不變的。也就是說碰到Checked異常,要不使用throws將其拋出,要麼使用try/catch語句塊捕獲處理。

自定義異常

一般來說吧,我們不會去手動拋出異常,當然,這裡說的異常指的是系統級別的異常,那除此之外,我們還可以自己自定義異常,代碼如下:

class MyException extends Exception {
    public MyException(){}

    public MyException(String msg) {
        super(msg);
    }
}

以上我們就自定義瞭一個異常,自定義異常我們需要註意以下兩點:

創建一個無參構造器創建一個帶有字符串參數的有參構造器

這裡的字符串參數其實就是異常的具體描述信息,比如我們之前這樣定義一個異常:

一般的我們要是自定義異常的話最好就是有一個“見名知意”的程度,就是我看到你這個自定義異常類名,大概知道這是一個什麼異常。

小結

以上我們就Java中的異常進行瞭學習,不知道你發現沒有,我們對異常的學習其實主要就是在圍繞以下五個關鍵字:

trycatchfinallythrowsthrow

然後還有就是要註意Checked異常和Runtime異常,以上都是關於異常的基本知識,掌握這些,足以應付我們在日常工作學習中異常操作,至於更深層次的學習則需要我們在實際應用的去不斷的探索瞭,關於Java中的異常,我們就先介紹到這裡。

到此這篇關於Java異常的文章就介紹到這瞭,更多相關Java異常內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: