天天看點

淺談java異常[Exception]類,一文帶你搞懂

一. 異常的定義

在《java程式設計思想》中這樣定義 異常:阻止目前方法或作用域繼續執行的問題。雖然java中有異常處理機制,但是要明确一點,決不應該用"正常"的态度來看待異常。絕對一點說異常就是某種意義上的錯誤,就是問題,它可能會導緻程式失敗。之是以java要提出異常處理機制,就是要告訴開發人員,你的程式出現了不正常的情況,請注意。

記得當初學習java的時候,異常總是搞不太清楚,不知道這個異常是什麼意思,為什麼會有這個機制?但是随着知識的積累逐漸也對異常有一點感覺了。舉一個例子來說明一下異常的用途。

public` `class` `Calculator {``  ``public` `int` `devide(``int` `num1, ``int` `num2) {``    ``//判斷除數是否為0``    ``if``(num2 == ``0``) {``      ``throw` `new` `IllegalArgumentException(``"除數不能為零"``);``    ``}``    ` `    ``return` `num1/num2;``  ``}``}
           

看一下這個類中關于除運算的方法,如果你是新手你可能會直接傳回計算結果,根本不去考慮什麼參數是否正确,是否合法(當然可以原諒,誰都是這樣過來的)。但是我們應盡可能的考慮周全,把可能導緻程式失敗的"苗頭"扼殺在搖籃中,是以進行參數的合法性檢查就很有必要了。其中執行參數檢查抛出來的那個參數非法異常,這就屬于這個方法的不正常情況。正常情況下我們會正确的使用電腦,但是不排除粗心大意把除數指派為0。如果你之前沒有考慮到這種情況,并且恰巧使用者數學基礎不好,那麼你完了。但是如果你之前考慮到了這種情況,那麼很顯然錯誤已在你的掌控之中。

二. 異常掃盲行動

今天和别人聊天時看到一個笑話:世界上最真情的相依,是你在try我在catch。無論你發神馬脾氣,我都默默承受,靜靜處理。 大多數新手對java異常的感覺就是:try…catch…。沒錯,這是用的最多的,也是最實用的。我的感覺就是:java異常是從"try…catch…"走來。

首先來熟悉一下java的異常體系:

Throwable 類是 Java 語言中所有錯誤或異常的超類(這就是一切皆可抛的東西)。它有兩個子類:Error和Exception。

**Error:**用于訓示合理的應用程式不應該試圖捕獲的嚴重問題。這種情況是很大的問題,大到你不能處理了,是以聽之任之就行了,你不用管它。比如說VirtualMachineError:當 Java 虛拟機崩潰或用盡了它繼續操作所需的資源時,抛出該錯誤。好吧,就算這個異常的存在了,那麼應該何時,如何處理它呢??交給JVM吧,沒有比它更專業的了。

**Exception:**它指出了合理的應用程式想要捕獲的條件。Exception又分為兩類:一種是CheckedException,一種是UncheckedException。這兩種Exception的差別主要是CheckedException需要用try…catch…顯示的捕獲,而UncheckedException不需要捕獲。通常UncheckedException又叫做RuntimeException。《effective java》指出:對于可恢複的條件使用被檢查的異常(CheckedException),對于程式錯誤(言外之意不可恢複,大錯已經釀成)使用運作時異常(RuntimeException)。

我們常見的RuntimeExcepiton有IllegalArgumentException、IllegalStateException、NullPointerException、IndexOutOfBoundsException等等。對于那些CheckedException就不勝枚舉了,我們在編寫程式過程中try…catch…捕捉的異常都是CheckedException。io包中的IOException及其子類,這些都是CheckedException。

三. 異常的使用

在異常的使用這一部分主要是示範代碼,都是我們平常寫代碼的過程中會遇到的(當然隻是一小部分),抛磚引玉嗎!

例1. 這個例子主要通過兩個方法對比來示範一下有了異常以後代碼的執行流程。

public` `static` `void` `testException1() {``    ``int``[] ints = ``new` `int``[] { ``1``, ``2``, ``3``, ``4` `};``    ``System.out.println(``"異常出現前"``);``    ``try` `{``      ``System.out.println(ints[``4``]);``      ``System.out.println(``"我還有幸執行到嗎"``);``// 發生異常以後,後面的代碼不能被執行``    ``} ``catch` `(IndexOutOfBoundsException e) {``      ``System.out.println(``"數組越界錯誤"``);``    ``}``    ``System.out.println(``"異常出現後"``);``  ``}``  ``/*output:``  ``異常出現前``  ``數組越界錯誤``  ``4``  ``異常出現後``  ``*/
           
public` `static` `void` `testException2() {``    ``int``[] ints = ``new` `int``[] { ``1``, ``2``, ``3``, ``4` `};``    ``System.out.println(``"異常出現前"``);``    ``System.out.println(ints[``4``]);``    ``System.out.println(``"我還有幸執行到嗎"``);``// 發生異常以後,他後面的代碼不能被執行``  ``}
           

首先指出例子中的不足之處,IndexOutofBoundsException是一個非受檢異常,是以不用try…catch…顯示捕捉,但是我的目的是對同一個異常用不同的處理方式,看它會有什麼不同的而結果(這裡也就隻能用它将就一下了)。異常出現時第一個方法隻是跳出了try塊,但是它後面的代碼會照樣執行的。但是第二種就不一樣了直接跳出了方法,比較強硬。從第一個方法中我們看到,try…catch…是一種"事務性"的保障,它的目的是保證程式在異常的情況下運作完畢,同時它還會告知程式員程式中出錯的詳細資訊(這種詳細資訊有時要依賴于程式員設計)。

例2. 重新抛出異常

public` `class` `Rethrow {``  ``public` `static` `void` `readFile(String file) ``throws` `FileNotFoundException {``    ``try` `{``      ``BufferedInputStream in = ``new` `BufferedInputStream(``new` `FileInputStream(file));``    ``} ``catch` `(FileNotFoundException e) {``      ``e.printStackTrace();``      ``System.err.println(``"不知道如何處理該異常或者根本不想處理它,但是不做處理又不合适,這是重新抛出異常交給上一級處理"``);``      ``//重新抛出異常``      ``throw` `e;``    ``}``  ``}``  ` `  ``public` `static` `void` `printFile(String file) {``    ``try` `{``      ``readFile(file);``    ``} ``catch` `(FileNotFoundException e) {``      ``e.printStackTrace();``    ``}``  ``}``  ` `  ``public` `static` `void` `main(String[] args) {``    ``printFile(``"D:/file"``);``  ``}``}
           

異常的本意是好的,讓我們試圖修複程式,但是現實中我們修複的幾率很小,我們很多時候就是用它來記錄出錯的資訊。如果你厭倦了不停的處理異常,重新抛出異常對你來說可能是一個很好的解脫。原封不動的把這個異常抛給上一級,抛給調用這個方法的人,讓他來費腦筋吧。這樣看來,java異常(當然指的是受檢異常)又給我們平添很多麻煩,盡管它的出發點是好的。

例3. 異常鍊的使用及異常丢失

定義三個異常類:ExceptionA,ExceptionB,ExceptionC

public` `class` `ExceptionA ``extends` `Exception {``  ``public` `ExceptionA(String str) {``    ``super``();``  ``}``}``public` `class` `ExceptionB ``extends` `ExceptionA {``  ``public` `ExceptionB(String str) {``    ``super``(str);``  ``}``}``public` `class` `ExceptionC ``extends` `ExceptionA {``  ``public` `ExceptionC(String str) {``    ``super``(str);``  ``}``}
           

異常丢失的情況:

public` `class` `NeverCaught {``  ``static` `void` `f() ``throws` `ExceptionB{``    ``throw` `new` `ExceptionB(``"exception b"``);``  ``}``  ``static` `void` `g() ``throws` `ExceptionC {``    ``try` `{``      ``f();``    ``} ``catch` `(ExceptionB e) {``      ``ExceptionC c = ``new` `ExceptionC(``"exception a"``);``      ``throw` `c;``    ``}``  ``}``  ``public` `static` `void` `main(String[] args) {``      ``try` `{``        ``g();``      ``} ``catch` `(ExceptionC e) {``        ``e.printStackTrace();``      ``}``  ``}``}``/*``exception.ExceptionC``at exception.NeverCaught.g(NeverCaught.java:12)``at exception.NeverCaught.main(NeverCaught.java:19)``*/
           

為什麼隻是列印出來了ExceptionC而沒有列印出ExceptionB呢?這個還是自己分析一下吧!

上面的情況相當于少了一種異常,這在我們排錯的過程中非常的不利。那我們遇到上面的情況應該怎麼辦呢?這就是異常鍊的用武之地:儲存異常資訊,在抛出另外一個異常的同時不丢失原來的異常。

public` `class` `NeverCaught {``  ``static` `void` `f() ``throws` `ExceptionB{``    ``throw` `new` `ExceptionB(``"exception b"``);``  ``}``  ``static` `void` `g() ``throws` `ExceptionC {``    ``try` `{``      ``f();``    ``} ``catch` `(ExceptionB e) {``      ``ExceptionC c = ``new` `ExceptionC(``"exception a"``);``      ``//異常連``      ``c.initCause(e);``      ``throw` `c;``    ``}``  ``}``  ``public` `static` `void` `main(String[] args) {``      ``try` `{``        ``g();``      ``} ``catch` `(ExceptionC e) {``        ``e.printStackTrace();``      ``}``  ``}``}``/*``exception.ExceptionC``at exception.NeverCaught.g(NeverCaught.java:12)``at exception.NeverCaught.main(NeverCaught.java:21)``Caused by: exception.ExceptionB``at exception.NeverCaught.f(NeverCaught.java:5)``at exception.NeverCaught.g(NeverCaught.java:10)``... 1 more``*/
           

這個異常鍊的特性是所有異常均具備的,因為這個initCause()方法是從Throwable繼承的。

例4. 清理工作

清理工作對于我們來說是必不可少的,因為如果一些消耗資源的操作,比如IO,JDBC。如果我們用完以後沒有及時正确的關閉,那後果會很嚴重,這意味着記憶體洩露。異常的出現要求我們必須設計一種機制不論什麼情況下,資源都能及時正确的清理。這就是finally。

public` `void` `readFile(String file) {``    ``BufferedReader reader = ``null``;``    ``try` `{``      ``reader = ``new` `BufferedReader(``new` `InputStreamReader(``          ``new` `FileInputStream(file)));``      ``// do some other work``    ``} ``catch` `(FileNotFoundException e) {``      ``e.printStackTrace();``    ``} ``finally` `{``      ``try` `{``        ``reader.close();``      ``} ``catch` `(IOException e) {``        ``e.printStackTrace();``      ``}``    ``}``  ``}
           

例子非常的簡單,是一個讀取檔案的例子。這樣的例子在JDBC操作中也非常的常見。(是以,我覺得對于資源的及時正确清理是一個程式員的基本素質之一。)

Try…finally結構也是保證資源正确關閉的一個手段。如果你不清楚代碼執行過程中會發生什麼異常情況會導緻資源不能得到清理,那麼你就用try對這段"可疑"代碼進行包裝,然後在finally中進行資源的清理。舉一個例子:

public` `void` `readFile() {``    ``BufferedReader reader = ``null``;``    ``try` `{``      ``reader = ``new` `BufferedReader(``new` `InputStreamReader(``          ``new` `FileInputStream(``"file"``)));``      ``// do some other work``    ` `      ``//close reader``      ``reader.close();``    ``} ``catch` `(FileNotFoundException e) {``      ``e.printStackTrace();``    ``} ``catch` `(IOException e) {``      ``e.printStackTrace();``    ``}``  ``}
           

我們注意一下這個方法和上一個方法的差別,下一個人可能習慣更好一點,及早的關閉reader。但是往往事與願違,因為在reader.close()以前異常随時可能發生,這樣的代碼結構不能預防任何異常的出現。因為程式會在異常出現的地方跳出,後面的代碼不能執行(這在上面應經用執行個體證明過)。這時我們就可以用try…finally來改造:

public` `void` `readFile() {``    ``BufferedReader reader = ``null``;``    ``try` `{``      ``try` `{``        ``reader = ``new` `BufferedReader(``new` `InputStreamReader(``            ``new` `FileInputStream(``"file"``)));``        ``// do some other work``        ``// close reader``      ``} ``finally` `{``        ``reader.close();``      ``}``    ``} ``catch` `(FileNotFoundException e) {``      ``e.printStackTrace();``    ``} ``catch` `(IOException e) {``      ``e.printStackTrace();``    ``}``  ``}
           

及早的關閉資源是一種良好的行為,因為時間越長你忘記關閉的可能性越大。這樣在配合上try…finally就保證萬無一失了(不要嫌麻煩,java就是這麼中規中矩)。

再說一種情況,假如我想在構造方法中打開一個檔案或者建立一個JDBC連接配接,因為我們要在其他的方法中使用這個資源,是以不能在構造方法中及早的将這個資源關閉。那我們是不是就沒轍了呢?答案是否定的。看一下下面的例子:

public` `class` `ResourceInConstructor {``  ``BufferedReader reader = ``null``;``  ``public` `ResourceInConstructor() {``    ``try` `{``      ``reader = ``new` `BufferedReader(``new` `InputStreamReader(``new` `FileInputStream(``""``)));``    ``} ``catch` `(FileNotFoundException e) {``      ``e.printStackTrace();``    ``}``  ``}``  ` `  ``public` `void` `readFile() {``    ``try` `{``      ``while``(reader.readLine()!=``null``) {``        ``//do some work``      ``}``    ``} ``catch` `(IOException e) {``      ``e.printStackTrace();``    ``}``  ``}``  ` `  ``public` `void` `dispose() {``    ``try` `{``      ``reader.close();``    ``} ``catch` `(IOException e) {``      ``e.printStackTrace();``    ``}``  ``}``}
           

這一部分講的多了一點,但是異常确實是看起來容易用起來難的東西呀,java中還是有好多的東西需要深挖的。

四. 異常的誤用

對于異常的誤用着實很常見,上一部分中已經列舉了幾個,大家仔細的看一下。下面再說兩個其他的。

例1. 用一個Exception來捕捉所有的異常,頗有"一夫當關萬夫莫開"的氣魄。不過這也是最傻的行為。

public` `void` `readFile(String file) {``    ``BufferedReader reader = ``null``;``    ``Connection conn = ``null``;``    ``try` `{``      ``reader = ``new` `BufferedReader(``new` `InputStreamReader(``          ``new` `FileInputStream(file)));``      ``// do some other work``      ` `      ``conn = DriverManager.getConnection(``""``);``      ``//...``    ``} ``catch` `(Exception e) {``      ``e.printStackTrace();``    ``} ``finally` `{``      ``try` `{``        ``reader.close();``        ``conn.close();``      ``} ``catch` `(Exception e) {``        ``e.printStackTrace();``      ``}``    ``}``  ``}
           

從異常角度來說這樣嚴格的程式确實是萬無一失,所有的異常都能捕獲。但是站在程式設計人員的角度,萬一這個程式出錯了我們該如何分辨是到底是那引起的呢,IO還是JDBC…是以,這種寫法很值得當做一個反例。大家不要以為這種做法很幼稚,傻子才會做。我在公司實習時确實看見了類似的情況:隻不過是人家沒有用Exception而是用了Throwable。

例2. 這裡就不舉例子了,上面的程式都是反例。異常是程式處理意外情況的機制,當程式發生意外時,我們需要盡可能多的得到意外的資訊,包括發生的位置,描述,原因等等。這些都是我們解決問題的線索。但是上面的例子都隻是簡單的printStackTrace()。如果我們自己寫代碼,就要盡可能多的對這個異常進行描述。比如說為什麼會出現這個異常,什麼情況下會發生這個異常。如果傳入方法的參數不正确,告知什麼樣的參數是合法的參數,或者給出一個sample。

例3. 将try block寫的簡短,不要所有的東西都扔在這裡,我們盡可能的分析出到底哪幾行程式可能出現異常,隻是對可能出現異常的代碼進行try。盡量為每一個異常寫一個try…catch,避免異常丢失。在IO操作中,一個IOException也具有"一夫當關萬夫莫開"的氣魄。

五.總結

總結非常簡單,不要為了使用異常而使用異常。異常是程式設計的一部分,對它的設計也要考究點。