1. 程式人生 > >JAVA基礎——異常--解析

JAVA基礎——異常--解析

ioe 沒有 lin 用戶 extend 結果 err lean 後者

簡介

異常處理是java語言的重要特性之一,《Three Rules for effective Exception Handling》一文中是這麽解釋的:它主要幫助我們在debug的過程中解決下面的三個問題。

  • 什麽出錯了
  • 哪裏出錯了
  • 為什麽出錯

java語言可以說是提供了過於完善的異常處理機制,以致於後來《Thinking in java》的作者Bruce Eckel都專門對他進行了論述。java中的異常機制包括ErrorException兩個部分。他們都繼承自一個共同的基類Throwable。Error屬於JVM運行中發生的一些錯誤,雖然並不屬於開發人員的範疇,但是有些Error還是由代碼引起的,比如StackOverflowError經常由遞歸操作引起,這種錯誤就是告訴開發者,你一般無法挽救,只能靠JVM。而Exception假設程序員會去處理這些異常,比如數據庫連接出了異常,那麽我們可以處理這個異常,並且重新連接等。Exception分為兩種,檢查類型(checked)和未檢查類型(unchecked)。檢查類型的異常就是說要程序員明確的去聲明或者用try..catch語句來處理的異常,而非檢查類型的異常則沒有這些限制,比如我們常見的 NullPointerException 就是非檢查類型的,他繼承自RuntimeException。java是目前主流編程語言中唯一一個推崇使用檢查類型異常的,至少sun是這樣的。關於使用checked還是unchecked異常的論戰一直很激烈。下面是一張java語言中異常的類關系圖。

技術分享圖片

基本使用

我們在使用java的一些文件或者數據庫操作的時候已經接觸過一些異常了,比如IOException、SQLException等,這些方法被聲明可能會拋出某種異常,因此我們需要對其進行捕獲處理。這就需要基本的try..catch語句了。下圖就是我們經常寫的一個基本結構。try語句塊中寫可能會拋出異常的代碼,之後在catch語句塊中進行捕獲。我們看到catch的參數寫的是一個Exception對象,這就意味著這個語句塊可以捕獲所有的檢查類型的異常(雖然這並不是一種好的寫法,稍後討論),finally總是會保證在最後執行,一般我們在裏面處理一些清理的工作,比如關閉文件流或者數據庫,網絡等操作。

技術分享圖片

當然上面的語句塊結構是靈活的,但是try是必須有的,catch和finally兩者至少有一個,當然catche的數量可以有多個。有時候try語句塊中可能拋出多種類型的異常,這個時候,我們可以寫多個catch語句來捕獲不同類型的異常,一個比較好的寫法如下:

技術分享圖片 技術分享圖片
        try{
            // ..invoke some methods that may throw exceptions
        }catch(ExceptionType1 e){
            //...handle exception
        }catch(ExceptionType2 e){
            //...handle exception
        }catch(Exception e){
            //...handle exception
        }finally{
            //..do some cleaning :close the file db etc.
        }
技術分享圖片 技術分享圖片

當異常不滿足前兩個type的時候,exception會將異常捕獲。我們發現這個寫法比較類似switch case的結構控制語句,但實際上,一旦某個catch得到匹配後,其他的就不會就匹配了,有點像加了break的case。有一點需要註意catch(Exception)一定要寫在最後面,catch是順序匹配的,後面匹配Exception的子類,編譯器就會報錯。

初次學習try..catch總會被其吸引,所以大量的使用這種結果,以達到某種“魯棒性”。(這語句也是程序員表白的最愛)。但try語句實際上執行的時候會導致棧操作。即要保存整個方法的調用路徑,這勢必會使得程序變慢。fillInStackTrace()是Throwable的一個方法,用來執行棧的操作,他是線程同步的,本身也很耗時。這裏問題在StackOverFlow上曾經有過一段非常經典的討論,原文。 的確當我們在try中什麽都不做,或者只執行一個類似加法的簡單調用,那麽其執行效率和goto這樣的控制語句是幾乎一樣的。但是誰會寫這樣的代碼呢?

總之不要總是試圖通過try catch來控制程序的結構,無論從效率還是代碼的可讀性上都不好。

try catch好的一面

try catch雖然不推薦用於程序結構的控制,但是也具有重要的意義,其設計的一個好處就是,開發人員可以把一件事情當做事務來處理,事務也是數據庫中重要的概念,舉個例子,比如完成訂單的這個事務,其中包括了一個動作序列,包括用戶提交訂單,商品出庫,關聯等。當這個序列中某一個動作執行失敗的時候,數據統一恢復到一個正常的點,這樣就不會出現,你付完了帳,商品卻沒有給你的情況。我們在try語句塊中就像執行一個事務一樣,當出現了異常,就會在catch中得到統一的處理,保證數據的完整無損。其實很多不好的代碼也是因為沒有好好利用catch語句的語言,導致很多異常就被淹沒了,這個後面介紹。

定制詳細的異常

我們可以自己定義異常,以捕獲處理某個具體的例子。創建自己的異常類,可以直接繼承Exception或者RuntimeException。區別是前者是簡稱類型的,而後者為檢查類型異常。Sun官方力挺傳統的觀點,他建議開發者都是用檢查類型的異常,即你一定要去處理的異常。下面是定義的一個簡單的異常類.

技術分享圖片 技術分享圖片
public class SimpleException extends Exception{

    SimpleException(){}
    SimpleException(String info){
        super(info);
    }
}
技術分享圖片 技術分享圖片

我們覆寫了兩個構造方法,這是有意義的。通過傳遞字符串參數,我們創建一個異常對象的時候,可以記錄下詳細的信息,這樣這個異常被捕獲的時候就會顯示我們之前定義的詳細信息。比如用下面的代碼測試一下我們定義的異常類:

技術分享圖片 技術分享圖片
public class Test {

    public void fun() throws SimpleException{
        throw new SimpleException("throwing from fun");
    }
    public static void main(String[] args) {
        Test t = new Test();
        try{
            t.fun();
        }catch(SimpleException e){
            e.printStackTrace();
        }
    }
}
技術分享圖片 技術分享圖片

運行就會得到下面的結果 printStackTrace是打印調用棧的方法,他有三個重載方法,默認的是將信息輸出到System.err。這樣我們就可以清晰的看到方法調用的過程,有點像操作系統中的中斷,保護現場。

SimpleException: throwing from fun
at Test.fun(Test.java:4)
at Test.main(Test.java:9)

略微麻煩的語法

我們自己實現的異常有時候會用到繼承這些特性,在異常繼承的時候有一些限制。那就是子類不能拋出基類或所實現的接口中沒有拋出的異常.比如有如下的接口:

public interface InterfaceA {
    public void f() throws IOException;
}

我們的Test類實現這個接口,那麽Test的f方法要麽不拋出異常,要麽只能拋出IOException,其實關於這裏還有更瑣碎的規矩,詳細可以參考《Java Puzzlers》第37個謎題。所以這和傳統的繼承和實現接口正好相反,面向對象的繼承是擴大化,而這正好是縮小了。

關於checked和unchecked的論戰

傳統的觀點裏,sun認為"因為 Java 語言並不要求方法捕獲或者指定運行時異常,因此編寫只拋出運行時異常的代碼或者使得他們的所有異常子類都繼承自 RuntimeException ,對於程序員來說是有吸引力的。這些編程捷徑都允許程序員編寫 Java 代碼而不會受到來自編譯器的所有挑剔性錯誤的幹擾,並且不用去指定或者捕獲任何異常。 盡管對於程序員來說這似乎比較方便,但是它回避了 Java 的捕獲或者指定要求的意圖,並且對於那些使用您提供的類的程序員可能會導致問題。"他強調盡量不使用unchecked異常。

但《Thinking in java》的作者Eckel卻改變了自己的想法, 他在自己博客上的一篇文章(這篇文章很好,表達也很簡單)專門列舉了使用checked異常的弊端。他指出正式檢查類型讓導致了很多的異常不能被程序員發現。開發人員有更大的自由去決定是不是要處理一個異常。即使忘記處理了某個異常,他也會在某個地方拋出來被發現,而不至於丟失。checked異常使得代碼的可讀性變差,並且正在暗暗的鼓勵人們去淹沒異常。現在很多IDE都在提醒我們,某個方法要跑出異常,然後甚至自動幫我們生成catch或者throw。這是非常可怕的行為,這導致了我們很多catch語句裏面什麽都沒有,就像一個陷阱一樣。

checked異常帶來的另一個問題是,代碼的難維護性,因為要在方法聲明上加上throws,如果方法的實現發生了某個變化,有了新的異常,那麽我們不得不去修改方法的聲明。還有一點不好的就是不能明確的暴露異常的特征。比如我們登錄成績系統的時候,如果用戶名註冊,我們可能期待一個NoSuchStudentException但是實際看到的可能是一個SQLException。《Effective java》中第 43 條:拋出與抽象相適應的異常。講的就是這個原則,即拋出的異常應該是和抽象的概念一致的,比如我們在一個系統無論遇到什麽具體的問題,但是大部分我們看到的都只是SQLException而已。

關於如何選擇,Bloch的建議是為可恢復的條件使用檢查型異常,為編程錯誤使用運行時異常。我的感覺是選擇檢查的異常就一定要”處理“,當然此處的處理一定是真正的處理而不是空寫一個catch語句而已。不知道未來的java會怎樣對待checked和unchecked,畢竟現在java是唯一一個支持檢查異常的主流編程語言了。

好的原則

Fail Fast:就是要盡早的拋出異常,這樣有有助於更加精確的定位出錯的地點和原因。這個也比較好理解,比如用戶名字不合法的時候馬上拋出,UserNameIllegalException,如果沒有及時拋出異常,那麽不合法的名字可能會導致一個SQLException,但是程序報給你一個SQLException,你卻很難直接得知一定是用戶名不合法造成的。Fail Fast這種思想,在java實現ArrayList的機制中也有很好的體現。

Catch late:不要在方法內部過早的處理異常,特別是什麽也不做的處理,那就更加的可怕了。因為如果“無作為”的處理很可能導致後面繼續出現新的異常(比如錯誤的用戶名會引發後面一些列錯誤,程序還不能處理好錯誤的用戶名,後面的就更處理不了了),這就給調試增加了很大的困難。一個好的經驗是將異常處理交給調用者,方法只在及時的地方拋出異常,技術上實現的方式就是給方法聲明throws,標出所有可能要拋出的異常。

JAVA基礎——異常--解析