1. 程式人生 > >Java兩種處理異常方法的區別

Java兩種處理異常方法的區別

在介紹異常處理之前我們先了解一下異常

我的部落格

什麼是異常

簡單來說,java程式在執行期間發生的問題就是異常。

在Java中,把異常資訊封裝成了一個類,當出現了問題時,就會建立異常類物件並丟擲異常相關資訊(如異常出現的位置、原因等等)。

在Java中使用Exception類來描述異常。字面翻譯就是“意外、例外”的意思,也就是非正常情況。事實上,異常本質上是程式上的錯誤,包括程式邏輯錯誤和系統錯誤。比如使用空的引用、陣列下標越界、記憶體溢位錯誤等,這些都是意外的情況,背離我們程式本身的意圖。錯誤在我們編寫程式的過程中會經常發生,包括編譯期間和執行期間的錯誤,在編譯期間出現的錯誤有編譯器幫助我們一起修正,然而執行期間的錯誤便不是編譯器力所能及了,並且執行期間的錯誤往往是難以預料的。

假若程式在執行期間出現了錯誤,如果置之不理,程式便會終止或直接導致系統崩潰,顯然這不是我們希望看到的結果。因此,如何對執行期間出現的錯誤進行處理和補救呢?Java提供了異常機制來進行處理,通過異常機制來處理程式執行期間出現的錯誤。

java異常類的分類

Exception類及其子類是Throwable的一種形式,Throwable類是Java語言中所有錯誤或異常的超類,即祖宗類,在Java中定義了很多異常類(如OutOfMemoryError、NullPointerException、IndexOutOfBoundsException等),這些異常類分為兩大類:ErrorException

Error是無法處理的異常,比如OutOfMemoryError,一般發生這種異常,JVM會選擇終止程式。因此我們編寫程式時不需要關心這類異常。
  
Exception也就是我們經常見到的一些異常情況,比如NullPointerException、IndexOutOfBoundsException,這些異常是我們可以處理的異常。
  
Exception類的異常包括checked exception和unchecked exception(unchecked exception也稱執行時異常RuntimeException,當然這裡的執行時異常並不是前面我所說的執行期間的異常,只是Java中用執行時異常這個術語來表示,Exception類的異常都是在執行期間發生的)。
  
unchecked exception

(非檢查異常),也稱執行時異常(RuntimeException),比如常見的NullPointerException、IndexOutOfBoundsException。對於執行時異常,java編譯器不要求必須進行異常捕獲處理或者丟擲宣告,由程式設計師自行決定。
  
checked exception(檢查異常),也稱非執行時異常(執行時異常以外的異常就是非執行時異常),java編譯器強制程式設計師必須進行捕獲處理,比如常見的IOExeption和SQLException。對於非執行時異常如果不進行捕獲或者丟擲宣告處理,編譯都不會通過。

典型的RuntimeException包括NullPointerException、IndexOutOfBoundsException、IllegalArgumentException等。
  
典型的非RuntimeException包括IOException、SQLException等。

Java中的異常處理

在Java中如果需要處理異常,必須先對異常進行捕獲,然後再對異常情況進行處理。如何對可能發生異常的程式碼進行異常捕獲和處理呢?使用try和catch關鍵字即可,如下面一段程式碼所示:

try {
  File file = new File("d:/a.txt");
  if(!file.exists())
    file.createNewFile();
} catch (IOException e) {
  // TODO: handle exception
}

被try塊包圍的程式碼說明這段程式碼可能會發生異常,一旦發生異常,異常便會被catch捕獲到,然後需要在catch塊中進行異常處理。

這是一種處理異常的方式。在Java中還提供了另一種異常處理方式即丟擲異常,顧名思義,也就是說一旦發生異常,我把這個異常丟擲去,讓呼叫者去進行處理,自己不進行具體的處理,此時需要用到throw和throws關鍵字。

下面看一個示例:

public class Main {
    public static void main(String[] args) {
        try {
            createFile();
        } catch (Exception e) {
            // TODO: handle exception
        }
    }

    public static void createFile() throws IOException{
        File file = new File("d:/a.txt");
        if(!file.exists())
            file.createNewFile();
    }
}

這段程式碼和上面一段程式碼的區別是,在實際的createFile方法中並沒有捕獲異常,而是用throws關鍵字宣告丟擲異常,即告知這個方法的呼叫者此方法可能會丟擲IOException。那麼在main方法中呼叫createFile方法的時候,採用try…catch塊進行了異常捕獲處理。

當然還可以採用throw關鍵字手動來丟擲異常物件。下面看一個例子:

public class Main {
    public static void main(String[] args) {
        try {
            int[] data = new int[]{1,2,3};
            System.out.println(getDataByIndex(-1,data));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

    }

    public static int getDataByIndex(int index,int[] data) {
        if(index<0||index>=data.length)
            throw new ArrayIndexOutOfBoundsException("陣列下標越界");
        return data[index];
    }
}

然後在catch塊中進行捕獲。

Java語言異常處理,對於可能會發生異常的程式碼,可以選擇以下三種方法來進行異常處理:

  1. 對程式碼塊用try..catch進行異常捕獲處理;

  2. 在 該程式碼的方法體外用throws進行丟擲宣告,告知此方法的呼叫者這段程式碼可能會出現這些異常,你需要謹慎處理。此時有兩種情況:

    • 如果宣告丟擲的異常是非執行時異常,此方法的呼叫者必須顯示地用try..catch塊進行捕獲或者繼續向上層丟擲異常。

    • 如果宣告丟擲的異常是執行時異常,此方法的呼叫者可以選擇地進行異常捕獲處理。

  3. 在程式碼塊用throw手動丟擲一個異常物件,此時也有兩種情況,跟2中的類似

    • 如果丟擲的異常物件是非執行時異常,此方法的呼叫者必須顯示地用try..catch塊進行捕獲或者繼續向上層丟擲異常。

    • 如果丟擲的異常物件是執行時異常,此方法的呼叫者可以選擇地進行異常捕獲處理。

PS:如果最終將異常拋給main方法,則相當於交給jvm自動處理,此時jvm會簡單地列印異常資訊

try…catch 是捕獲異常(自己處理)
throw 是丟擲異常(交給上一級處理)
你可以試著在一個類丟擲一個異常,再去別的類呼叫該類的例項。

自己處理指的是 在當前類處理。
交上一級指的是 來別的類呼叫該類時在別的類處理。
相對於處理異常來說,沒什麼區別;相對於類來說就這點區別了,
一個是自己發現問題自己處理,一個是告訴別人這有問題,讓別人去處理。

關於try、catch、finally、throws 和 throw

下面我們來看看java異常處理所用到的這些關鍵字的用法。

try,catch,finally

try關鍵字用來包圍可能會出現異常的邏輯程式碼,它單獨無法使用,必須配合catch或者finally使用。Java編譯器允許的組合使用形式只有以下三種形式:

try…catch…
try….finally……
try….catch…finally…

catch塊可以有多個,但是try塊只能有一個,finally塊是可選的(但是最多隻能有一個finally塊)。

三個塊執行的順序為try—>catch—>finally。

當然如果沒有發生異常,catch塊不會執行。但是finally塊無論在什麼情況下都是會執行的(finally)。

在有多個catch塊的時候,是按照catch塊的先後順序進行匹配的,一旦異常型別被一個catch塊匹配,則不會與後面的catch塊進行匹配。

在使用try..catch..finally塊的時候,注意千萬不要在finally塊中使用return,因為finally中的return會覆蓋已有的返回值。下面看一個例子:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;


public class Main {
    public static void main(String[] args) {
        String str = new Main().openFile();
        System.out.println(str);

    }

    public String openFile() {
        try {
            FileInputStream inputStream = new FileInputStream("d:/a.txt");
            int ch = inputStream.read();
            System.out.println("aaa");
            return "step1";
        } catch (FileNotFoundException e) {
            System.out.println("file not found");
            return "step2";
        }catch (IOException e) {
            System.out.println("io exception");
            return "step3";
        }finally{
            System.out.println("finally block");
            //return "finally";
        }
    }
}

輸出結果為:

file not found 
finally block
step2

可以看出,在try塊中發生FileNotFoundException之後,就跳到第一個catch塊,列印”file not found”資訊,並將”step2”賦值給返回值,然後執行finally塊,最後將返回值返回。
  
從這個例子說明,無論try塊或者catch塊中是否包含return語句,都會執行finally塊。
  
如果將這個程式稍微修改一下,將finally塊中的return語句註釋去掉,執行結果是:

file not found 
finally block
finally

最後打印出的是”finally”,返回值被重新覆蓋了。
  
因此如果方法有返回值,切忌不要再finally中使用return,這樣會使得程式結構變得混亂.

throws和throw

  1. throws出現在方法的宣告中,表示該方法可能會丟擲的異常,然後交給上層呼叫它的方法程式處理,允許throws後面跟著多個異常型別。
      
  2. throw一般會用於程式出現某種邏輯時程式設計師主動丟擲某種特定型別的異常。throw只會出現在方法體中,當方法在執行過程中遇到異常情況時,將異常資訊封裝為異常物件,然後throw出去。throw關鍵字的一個非常重要的作用就是 異常型別的轉換。
      
    PS: throws表示出現異常的一種可能性,並不一定會發生這些異常;throw則是丟擲了異常,執行throw則一定丟擲了某種異常物件。兩者都是消極處理異常的方式(這裡的消極並不是說這種方式不好),只是丟擲或者可能丟擲異常,但是不會由方法去處理異常,真正的處理異常由此方法的上層呼叫處理。

繼承中的異常處理

關於子類重寫父類方法,主要通過三條原則來確定異常丟擲宣告的型別。

  1. 父類的方法沒有宣告異常,子類在重寫該方法的時候不能宣告異常;
      
  2. 如果父類的方法宣告一個異常exception1,則子類在重寫該方法的時候宣告的異常不能是exception1的父類;
      
  3. 如果父類的方法宣告的異常型別只有非執行時異常(執行時異常),則子類在重寫該方法的時候宣告的異常也只能有非執行時異常(執行時異常),不能含有執行時異常(非執行時異常)。

前人筆記

以下是根據前人總結的一些異常處理的建議:

只在必要使用異常的地方才使用異常,不要用異常去控制程式的流程
  
謹慎地使用異常,,異常使用過多會嚴重影響程式的效能。如果在程式中能夠用if語句和Boolean變數來進行邏輯判斷,那麼儘量減少異常的使用,從而避免不必要的異常捕獲和處理。比如下面這段經典的程式:

public void useExceptionsForFlowControl() {  
  try {  
  while (true) {  
    increaseCount();  
    }  
  } catch (MaximumCountReachedException ex) {  
  }  
  //Continue execution  
}  

public void increaseCount() throws MaximumCountReachedException {  
  if (count >= 5000)  
    throw new MaximumCountReachedException();  
}

上邊的useExceptionsForFlowControl()用一個無限迴圈來增加count直到丟擲異常,這種做法並沒有說讓程式碼不易讀,而是使得程式執行效率降低。

切忌使用空catch塊

在捕獲了異常之後什麼都不做,相當於忽略了這個異常。千萬不要使用空的catch塊,空的catch塊意味著你在程式中隱藏了錯誤和異常,並且很可能導致程式出現不可控的執行結果。如果你非常肯定捕獲到的異常不會以任何方式對程式造成影響,最好用Log日誌將該異常進行記錄,以便日後方便更新和維護。

檢查異常和非檢查異常的選擇

一旦你決定丟擲異常,你就要決定丟擲什麼異常。這裡面的主要問題就是丟擲檢查異常還是非檢查異常。

檢查異常導致了太多的try…catch程式碼,可能有很多檢查異常對開發人員來說是無法合理地進行處理的,比如SQLException,而開發人員卻不得不去進行try…catch,這樣就會導致經常出現這樣一種情況:邏輯程式碼只有很少的幾行,而進行異常捕獲和處理的程式碼卻有很多行。這樣不僅導致邏輯程式碼閱讀起來晦澀難懂,而且降低了程式的效能。

我個人建議儘量避免檢查異常的使用,如果確實該異常情況的出現很普遍,需要提醒呼叫者注意處理的話,就使用檢查異常;否則使用非檢查異常。

因此,在一般情況下,我覺得儘量將檢查異常轉變為非檢查異常交給上層處理。

注意catch塊的順序

不要把上層類的異常放在最前面的catch塊。比如下面這段程式碼:

try {
        FileInputStream inputStream = new FileInputStream("d:/a.txt");
        int ch = inputStream.read();
        System.out.println("aaa");
        return "step1";
    } catch (IOException e) {
        System.out.println("io exception");        
         return "step2";
    }catch (FileNotFoundException e) {
        System.out.println("file not found");          
        return "step3";
    }finally{
        System.out.println("finally block");
        //return "finally";
    }

第二個catch的FileNotFoundException將永遠不會被捕獲到,因為FileNotFoundException是IOException的子類。

不要將提供給使用者看的資訊放在異常資訊裡

比如下面這段程式碼:

public class Main {
    public static void main(String[] args) {
        try {
            String user = null;
            String pwd = null;
            login(user,pwd);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

    }

    public static void login(String user,String pwd) {
        if(user==null||pwd==null)
            throw new NullPointerException("使用者名稱或者密碼為空");
        //...
    }
}

展示給使用者錯誤提示資訊最好不要跟程式混淆一起,比較好的方式是將所有錯誤提示資訊放在一個配置檔案中統一管理。

避免多次在日誌資訊中記錄同一個異常

只在異常最開始發生的地方進行日誌資訊記錄。很多情況下異常都是層層向上跑出的,如果在每次向上丟擲的時候,都Log到日誌系統中,則會導致無從查詢異常發生的根源。

異常處理儘量放在高層進行

儘量將異常統一拋給上層呼叫者,由上層呼叫者統一之時如何進行處理。如果在每個出現異常的地方都直接進行處理,會導致程式異常處理流程混亂,不利於後期維護和異常錯誤排查。由上層統一進行處理會使得整個程式的流程清晰易懂。

在finally中釋放資源

如果有使用檔案讀取、網路操作以及資料庫操作等,記得在finally中釋放資源。這樣不僅會使得程式佔用更少的資源,也會避免不必要的由於資源未釋放而發生的異常情況。

參考資料