讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議19:斷言絕對不是雞肋 >

建議19:斷言絕對不是雞肋

在防禦式編程中經常會用斷言(Assertion)對參數和環境做出判斷,避免程序因不當的輸入或錯誤的環境而產生邏輯異常,斷言在很多語言中都存在,C、C++、Python都有不同的斷言表示形式。在Java中的斷言使用的是assert關鍵字,其基本的用法如下:


assert<布爾表達式>

assert<布爾表達式>:<錯誤信息>


在布爾表達式為假時,拋出AssertionError錯誤,並附帶了錯誤信息。assert的語法較簡單,有以下兩個特性:

(1)assert默認是不啟用的

我們知道斷言是為調試程序服務的,目的是為了能夠快速、方便地檢查到程序異常,但Java在默認條件下是不啟用的,要啟用就需要在編譯、運行時加上相關的關鍵字,這就不多說,有需要的話可以參考一下Java規範。

(2)assert拋出的異常AssertionError是繼承自Error的

斷言失敗後,JVM會拋出一個AssertionError錯誤,它繼承自Error,注意,這是一個錯誤,是不可恢復的,也就表示這是一個嚴重問題,開發者必須予以關注並解決之。

assert雖然是做斷言的,但不能將其等價於if……else……這樣的條件判斷,它在以下兩種情況不可使用:

(1)在對外公開的方法中

我們知道防禦式編程最核心的一點就是:所有的外部因素(輸入參數、環境變量、上下文)都是「邪惡」的,都存在著企圖摧毀程序的罪惡本源,為了抵制它,我們要在程序中處處檢驗,滿地設卡,不滿足條件就不再執行後續程序,以保護主程序的正確性,處處設卡沒問題,但就是不能用斷言做輸入校驗,特別是公開方法。我們來看一個例子:


public class Client{

public static void main(Stringargs){

StringUtils.encode(null);

}}

//字符串處理工具類

class StringUtils{

public static String encode(String str){

assert str!=null:"加密的字符串為null";

/*加密處理*/

}

}


encode方法對輸入參數做了不為空的假設,如果為空,則拋出AssertionError錯誤,但這段程序存在一個嚴重的問題,encode是一個public方法,這標誌著是它對外公開的,任何一個類只要能夠傳遞一個String類型的參數(遵守契約)就可以調用,但是Client類按照規範和契約調用enocde方法,卻獲得了一個AssertionError錯誤信息,是誰破壞了契約協定?——是encode方法自己。

(2)在執行邏輯代碼的情況下

assert的支持是可選的,在開發時可以讓它運行,但在生產系統中則不需要其運行了(以便提高性能),因此在assert的布爾表達式中不能執行邏輯代碼,否則會因為環境不同而產生不同的邏輯,例如:


public void doSomething(List list, Object element){

assert list.remove(element):"刪除元素"+element+"失敗";

/*業務處理*/

}


這段代碼在assert啟用的環境下,沒有任何問題,但是一旦投入到生產環境,就不會啟用斷言了,而這個方法也就徹底完蛋了,list的刪除動作永遠都不會執行,所以也就永遠不會報錯或異常,因為根本就沒有執行嘛!

以上兩種情況下不能使用assert,那在什麼情況下能夠使用assert呢?一句話:按照正常執行邏輯不可能到達的代碼區域可以放置assert。具體分為三種情況:

(1)在私有方法中放置assert作為輸入參數的校驗

在私有方法中可以放置assert校驗輸入參數,因為私有方法的使用者是作者自己,私有方法的調用者和被調用者之間是一種弱契約關係,或者說沒有契約關係,其間的約束是依靠作者自己控制的,因此加上assert可以更好地預防自己犯錯,或者無意的程序犯錯。

(2)流程控制中不可能達到的區域

這類似於JUnit的fail方法,其標誌性的意義就是:程序執行到這裡就是錯誤的,例如:


public void doSomething(){

int i=7;

while(i>7){

/*業務處理*/

}

assert false:"到達這裡就表示錯誤";

}


(3)建立程序探針

我們可能會在一段程序中定義兩個變量,分別代表兩個不同的業務含義,但是兩者有固定的關係,例如var1=var2*2,那我們就可以在程序中到處設「樁」,斷言這兩者的關係,如果不滿足即表明程序已經出現了異常,業務也就沒有必要運行下去了。