做好例外處理,防止無故翻車

2020-09-28 12:01:54

絮叨

因為程式例外處理問題,就在前幾天龍叔的服務掛了幾秒鐘。

完了,馬上季度末打績效,竟然在這裡翻車了,心如刀絞啊。

雖然沒有影響到使用者體驗,但是找到問題並解決掉問題是工程師日常追求之一。

作為一個優秀的工程師,應該還得加幾點:

  • 弄清問題本質
  • 總結問題原因
  • 舉一反三,防止出現類似錯誤

例外處理,對於每個開發者來說一點不陌生。

有人這樣描述,「一個開發者90%的時間都是在處理程式異常」。

這樣說也不算是什麼過錯,畢竟正常的邏輯總是輕鬆容易的寫完,例外處理往往會佔據開發者大多數時間。

既然這麼佔據我們的開發時間,何不主動花點時間去瞭解他熟悉他,切莫讓他成為了最熟悉的陌生人。

文章大綱

異常分類

在Java中,異常分為受檢查的異常,與執行時異常。兩者都在異常類層次結構中。下面的圖展示了Java異常類的繼承關係。

不難看出所有的異常都繼承自一個共同的父類別Throwable,而Throwable有兩個重要的子類:Exception(異常)和Error(錯誤)。

Error(錯誤)

工程師最怕的就是Error,看到error和fail頭都大了三圈,感覺Error總是和我過不去。

Error是指Java 執行時系統的內部錯誤和資源耗盡錯誤。

應用程式不應該丟擲這種型別的物件。

如果出現了這樣的內部錯誤, 除了通告給使用者,並盡力使程式安全地終止之外, 再也無能為力了。一般這種情況很少出現。

這種錯誤會導致你的程式日常執行著,突然某天就夭折了。

Exception(異常)

異常指不期而至的各種狀況,如:檔案找不到、網路連線失敗、非法引數等。

異常是一個事件,它發生在程式執行期間,干擾了正常的指令流程。

Java通過API中Throwable類的眾多子類描述各種不同的異常。因而,Java異常都是物件,是Throwable子類的範例,描述了出現在一段編碼中的錯誤條件

當條件生成時,錯誤將引發異常。

異常和錯誤的區別:異常能被程式本身處理,錯誤是無法處理。

異常主要分為執行期異常非執行期異常(編譯異常)

執行期異常很好理解,就是程式跑著跑著因為觸發某個條件,導致異常發生了。比如越界了,NullPointerException等等。

編譯期異常,就是程式編譯時丟擲的異常,比如存取的檔案不存在。這類異常很好避免,編譯不會通過,不解決掉,程式就沒法執行起來。

當然有人也把異常分為可查異常不可查異常

可查異常 也稱之為編譯器要求必須處理的異常,一般編譯器都會檢查他,出現這類異常要麼用捕獲他,要麼丟擲他,總之必須處理他。

不可查異常 編譯器沒法檢查的,必須靠程式設計師去主動檢查,然後處理掉他。

分類的方法不是很重要,怎樣分取決於你處於某種情況下,最終都是要明白這些異常,並處理它。

例外處理機制

上面基本都明白了java異常是什麼,以及有哪些異常,下面我們就來聊聊用什麼樣的機制去處理這些異常。

八字方針 丟擲異常,捕捉異常

throw 語句用於拋出異常,throws 語句用於宣告可能會出現的異常。

舉個例子:

 public  Integer division(int x, int y) {
        if (y == 0)
            throw new ArithmeticException("丟擲算術異常"); //丟擲異常
        return x / y;
 }

throws丟擲異常的規則:

  1. 如果是不可查異常(unchecked exception),即Error、RuntimeException或它們的子類,那麼可以不使用throws關鍵字來宣告要丟擲的異常,編譯仍能順利通過,但在執行時會被系統丟擲。

2)必須宣告方法可丟擲的任何可查異常(checked exception)。即如果一個方法可能出現受可查異常,要麼用try-catch語句捕獲,要麼用throws子句宣告將它丟擲,否則會導致編譯錯誤

3)僅當丟擲了異常,該方法的呼叫者才必須處理或者重新丟擲該異常。當方法的呼叫者無力處理該異常的時候,應該繼續丟擲,而不是囫圇吞棗。

4)呼叫方法必須遵循任何可查異常的處理和宣告規則。若覆蓋一個方法,則不能宣告與覆蓋方法不同的異常。宣告的任何異常必須是被覆蓋方法所宣告異常的同類或子類。

java採用try-catch-finally語句來對異常進行捕獲並處理。

try{
	//可能產生異常的程式碼
}catch (Exception e){
  //例外處理邏輯
}
try{
	//可能產生異常的程式碼
}catch (Exception e){
  //例外處理邏輯
}finally {
  //必須執行的邏輯
}

這語法大家應該在熟悉不過了,算了,龍叔還是囉嗦一遍。

try語句塊:該語句塊中是程式正常情況下應該要完成的功能,而這些程式碼中可能會產生異常,其後面的catch語句塊就是用來捕獲並處理這些異常的。

catch語句塊:該語句塊用來捕獲並處理try語句塊中產生的異常。

每個catch語句塊宣告其能處理的一種特定型別的異常,catch後面的括號中就是該特定型別的異常。

在Java7以前,每個catch語句塊只能捕獲一種異常,從Java7開始就支援一個catch捕獲多種異常,多個異常之間用|隔開。

try{
            //可能會產生異常的程式碼
        }
        catch(Exception1 | Exception2 |... | Exception_n e1){
            //統一處理的異常程式碼
        }
        finally{
            //通常是釋放資源的程式碼
        }

finally塊:無論是否捕獲或處理異常,finally塊裡的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執行。

在以下4種特殊情況下,finally塊不會被執行:

1)在finally語句塊中發生了異常。
2)在前面的程式碼中用了System.exit()退出程式。
3)程式所在的執行緒死亡。
4)關閉CPU。

try、catch、finally語句塊的執行順序:

java異常處理執行順序

到這裡大家基本明白了異常怎麼來的,怎麼處理的,接下來說一個常見的異常遮蔽問題

一般情況下都是try中進行捕捉可能出現的異常,catch對異常進行處理,finally中進行一些資源關閉工作。

正常情況倒也沒啥說的,但咋就怕異常情況啊。

舉個例子:

try{
            //可能會產生異常的程式碼
        }
        catch(Exception1 | Exception2 |... | Exception_n e1){
            //統一處理的異常程式碼
        }
        finally{
            //通常是釋放資源的程式碼
          in.close();
          out.close();
        }

是不是日常都這麼寫,看起來蠻正常的。

如果我們的finally語句塊中也丟擲異常,會怎麼辦?

    public Integer division(int x, int y) throws Exception {
        try{
            return x/y;
        }catch (ArithmeticException e){
            System.out.println(e.getMessage());
            throw new ArithmeticException("算術異常");
        }finally {
            System.out.println("釋放資源");
            throw new Exception("釋放資源異常");
        }
    }

例如這段程式碼,本意是想丟擲算術運算異常 ,結果丟擲了釋放資源異常

由於異常資訊的丟失,異常遮蔽可能會導致某些bug變得極其難以發現,會讓你加班加到心態崩潰的。

這就是遮蔽異常,如何解決這種遮蔽異常?

有人看了上面的程式碼,又發現了另一個問題。try中有return語句,finally語句還會不會執行?

這個問題很好,答案是會執行,並且在方法返回撥用者前執行

解決遮蔽異常問題

Java 1.7中新增的try-with-resource語法糖來很好的解決這種因為關閉資源引起的異常遮蔽問題。

 public void testExcep(){
        BufferedInputStream in = null;
        BufferedOutputStream out = null;
        try {
            in = new BufferedInputStream(new FileInputStream(new File("in.txt")));
            out = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

為了釋放資源,我們不得不這樣寫。但當我們熟悉try-with-resource語法,我們可以這樣寫。

public static void main(String[] args) {
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(new File("in.txt")));
             BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
            //處理輸入資料並輸出
        } catch (IOException e) {
            //捕捉異常並處理
        }
    }

在try子句中能建立一個資源物件,當程式的執行完try-catch之後,執行環境自動關閉資源。

程式碼寫起來簡潔,也會解決掉遮蔽異常問題

當然也要注意,在使用try-with-resource的過程中,一定需要了解資源的close方法內部的實現邏輯。否則還是可能會導致資源洩露。

怎麼樣,是不是很簡單呢?學會了我們一起去裝逼

常見異常問題

算術異常類:ArithmeticExecption

空指標異常類:NullPointerException

型別強制轉換異常:ClassCastException

陣列負下標異常:NegativeArrayException

陣列下標越界異常:ArrayIndexOutOfBoundsException

檔案已結束異常:EOFException

檔案未找到異常:FileNotFoundException

字串轉換為數位異常:NumberFormatException

運算元據庫異常:SQLException

輸入輸出異常:IOException

方法未找到異常:NoSuchMethodException

這些都是非常常見的異常,當然還有一些其他異常,大家要在日常工作中及時總結,寫到你的小本本上。

今天的內容就到這裡了,有幫助記得點個贊👍。

我是龍叔,我們下期見✌️。