首頁>技術>

只針對異常的情況才使用異常

上面程式碼有什麼問題呢?

- 試圖透過丟擲異常並忽略的方式終止無限迴圈;意圖避免for迴圈的越界檢查;- 然而:1. 異常機制的設計初衷適用於不正常的情形,so,很少有JVM對其進行最佳化;2. 把程式碼放在try-cache中阻止了現代JVM實現本來可能要執行的某些特定最佳化;3. for遍歷並不會導致冗餘的檢查,有些現代的JVM會將它們最佳化掉;- 上面的程式碼模糊了程式碼意圖,降低了效能,還不能保證正常工作, 例如迴圈體中的計算過程呼叫了一個方法,這個方法執行了對某個不想關的陣列的越界訪問, 出發異常,而該try語句塊會忽略掉這個bug,增加了除錯過程的複雜性;

對api設計的啟發:設計良好的api不應該強迫它的客戶端為了正常的控制流而使用異常; 例如Iterator有個hasNext方法可以判斷是否終止迴圈,如果沒有提供,可能使用者就會不得不使用上面的程式碼實現了;

總之,異常就是為了異常情況下使用而設計,不要用於普通的控制流,也不要編寫迫使別人這樣做的API;

對可恢復的情況使用受檢異常,對程式設計錯誤使用執行時異常

java的三種可丟擲結構(throwable):- 受檢異常(checked exception)- 執行時異常(run-time exception)- 錯誤(error)

如果期望呼叫者能夠適當的恢復,則透過丟擲受檢異常,強迫呼叫者在一個catch子句中處理改異常或者將其傳播出去;

api的設計者讓api使用者面對受檢的異常,以此強制使用者從這個異常條件中回覆; 使用者可以忽略這樣的強制要求,只需要捕獲並忽略即可,但這往往不是一個好辦法; 如File,io流操作是常見的IOException,FileNotFoundException;

另外兩種則是不需要,也不應該被捕獲的;

用執行時異常表明程式設計錯誤;大多數表示前提違例( precondition violation);前提違例是指api使用者沒有遵守api規範建立的約定,如ArrayIndexOutOfBoundsException,NullPointerException;

錯誤被jvm保留用於表示資源不足,約束失敗,或其他使程式無法繼續執行的條件;因此最好不要實現任何新的Error子類;

api設計者往往會忘記,異常也是個完全意義上的物件,可以在它上面定義任意方法, 這些方法主要用於為捕獲異常的程式碼提供額外的資訊,受檢的異常往往指明瞭可恢復的條件, 所以對於這樣的異常,提供一些輔助方法尤為重要,可以幫助呼叫者獲得一些有助於恢復的資訊;

避免不必要的使用受檢異常

受檢異常強迫程式設計師處理,大大增強了可靠性;過分的使用受檢的異常會使api使用起來非常不方便(要做try-catch處理或丟擲);

所以設計api時要謹慎使用受檢異常,如果使用api的程式設計師無法做的比下面的更好,那麼使用未受檢的異常更為合適;

其實仔細回想一下,之前沒少寫上面這種程式碼,應該問問自己,是否有別的途徑來避免使用受檢異常;

一種解決方法是:把丟擲異常的方法分成兩個方法,其中一個返回boolean,即使用if判斷來處理是否丟擲異常的兩種情況,就如之前提過的Iterator有個hasNext方法;

(但是這樣可能會失去強制約束,api使用者不一定會呼叫如Iterator.hasNext()這種方法,這就需要完善的api說明文件來規範使用者的呼叫了)

重用現有異常的好處:1. 使api更加易於學習和使用(通用,習慣用法);

2. 對用到這些api的程式而言,可讀性會更好(不會出現很多不熟悉的異常);3. 異常類越少,記憶體印記(footprint)就越小,裝載這些類的時間開銷就越少;

如果希望增加更多的失敗-捕獲資訊,可以把現有的異常進行子類化;

常用異常:

- IllegalArgumentException: 非法引數(呼叫者傳參不合適時);

- IllegalStateException:非法狀態(被呼叫的程式中某個物件的狀態不滿足程式執行需求);

- NullPointerException: 這個就很常用了, 某個不允許為空的物件或引數為空時;

- IndexOutOfBoundsException: 這個也很熟悉了,運算元組時經常會遇到他的子類;

- ConcurrentModificationException: 如果一個物件被設計為專用於單執行緒或與外部同步機制配合使用,一旦發現它正在或已經被併發的修改,就應該丟擲這個異常;

- UnsupportedOperationException: 物件不支援使用者請求的方法;

丟擲與抽象相對應的異常

如果方法丟擲的異常與它所執行的任務沒有明顯的聯絡,將會使人不知所措;當放過傳遞由低層抽象丟擲的異常時,往往會發生這種情況,這也讓實現細節汙染了更高層的api;

異常轉譯:更高層的實現應該捕獲低層的異常,同時丟擲可以按照高層抽象進行解釋的異常;

異常鏈:如果低層異常對於除錯導致高層異常的問題非常有幫助,可以將低層異常傳到高層異常,高層異常提供訪問方法(Throwable.getCause())來獲取低層異常:

處理來自低層的異常最好的做法是:在呼叫低層方法之前確保他們會執行成功,從而避免他們丟擲異常,如檢查引數的有效性;

如果無法避免,次選方案是,讓高層來悄悄繞開這些異常,從而將高層方法的呼叫者與低層問題隔離,可以使用適當的記錄機制將異常記錄下來,有助於管理員調查問題;

每個方法丟擲的異常都要有文件

始終要單獨的宣告受檢的異常,並利用javadoc的@throws標記,準確的記錄下丟擲每個異常的條件;

一個方法需要丟擲多個異常類時,不要用這些異常的超類或Exception,Throwable,代替,這樣不僅沒 有提供"這個方法能夠丟擲哪些異常"的指導資訊,而且大大妨礙了該方法的使用,因為它實際上掩蓋了該方法在同樣的執行環境下可能丟擲的任何其他異常;

未受檢異常最好也使用javadoc的@throws標籤記錄,但不要使用throws關鍵字將未受檢的異常包含在方法的宣告中;

如果一個類中的許多方法處於同樣的原因而丟擲同一個異常,該類的文件註釋中對這個異常建立文件也是可以的;如"All methods in this class throw a NullPointerException if a null object reference is passed in any parameter";

在細節訊息中包含能捕獲失敗的資訊

為了捕獲失敗,異常的細節資訊應該包含所有"對該異常有貢獻"的引數和域的值; 當程式由於未被捕獲的異常而失敗的時候,系統會自動的打印出該異常的堆疊軌跡, 在堆疊軌跡中包含該異常的字串表示法,即toString方法的呼叫結果;

因為異常可能不易復現,所以toString(即細節資訊)中應該儘可能多的返回有關失敗的原因資訊; (為了捕獲失敗,異常的細節資訊應該包含所有"對該異常有貢獻"的引數和域的值) (堆疊軌跡的用途是與原始檔結合起來進行分析,它通常包含丟擲該異常的確切檔案和行數,以及堆疊中所有其他方法呼叫所在的檔案和行數)

異常的細節訊息不應該與“使用者層次的錯誤訊息”混為一談,後者對於終端使用者必須是可理解的,前者則用來讓程式設計師分析失敗原因,資訊內容比可理解性要重要的多; 為了確保足夠的資訊,一種拜訪是在異常的構造器中引入這些資訊 如:

努力使失敗保持原子性

定義:失敗的方法呼叫應該使物件保持在被呼叫之前的狀態,具有這種屬性的方法被稱為具有失敗原子性;

最簡單的辦法:設計一個不可變的物件;

對於可變物件的常見辦法:

1. 在執行操作之前檢查引數的有效性,這可以使得物件的狀態被修改之前, 先丟擲適當的異常,如我之前講棧的文章中ArrayStack的pop方法,如果取消對初始大小的檢查, 當這個方法企圖從一個空棧中彈出元素時,它仍然會丟擲異常, 而且,這將會導致將來對該物件的任何方法呼叫都會失敗;

2. 調整計算處理過程的順序,使得任何可能會失敗的計算部分都在物件狀態被修改之前發生;例如TreeMap的put方法, 向其中新增元素,該元素的型別就必須是可以利用TreeMap的排序準則與其他元素進行比較的, 如果企圖新增型別不正確的元素,在tree以任何方式被修改之前,自然會導致ClassCastException異常;

3. 編寫一段恢復程式碼,由它來攔截操作過程中發生的失敗,以及使物件回滾到操作開始之前的狀態上,這種辦法主要用於永久性的(基於磁碟的disk-based)資料結構;4. 在物件的一份臨時複製上執行操作,當操作完成後,再用臨時複製中的結果替代物件的內容,計算過程會更加迅速;

不要忽略異常

當API的設計者生命一個方法將丟擲某個異常的時候,他們等於正在試圖說明某些事情,所以請不要忽略它;

要忽略一個異常很容易,只需要如下這樣:

至少catch塊也應該包含一條說明,解釋為什麼可以忽略這個異常;

用空的catch塊忽略它,將會導致程式在遇到錯誤的情況下悄然的執行下去,然後在將來的某個點上, 當程式不能再容忍與錯誤源明顯相關的問題時,他就會失敗;

只要將異常傳播給外界,至少可以使程式迅速的失敗,從而保留了有助於除錯該失敗條件的資訊;

19
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 《ROS入門》第11講:(引數的使用與程式設計方法)