有沒有異常處理翻車過的,績效還被打了C

絮叨

因為程序異常處理問題,就在前幾天龍叔的服務掛了幾秒鐘。

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

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

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

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

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

有人這樣描述,「一個開發者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異常處理執行順序
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

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

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

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