讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議114:不要在構造函數中拋出異常 >

建議114:不要在構造函數中拋出異常

Java的異常機制有三種:

Error類及其子類表示的是錯誤,它是不需要程序員處理也不能處理的異常,比如VirtualMachineError虛擬機錯誤,ThreadDeath線程僵死等。

RuntimeException類及其子類表示的是非受檢異常,是系統可能會拋出的異常,程序員可以去處理,也可以不處理,最經典就是NullPointerException空指針異常和IndexOutOfBoundsException越界異常。

Exception類及其子類(不包含非受檢異常)表示的是受檢異常,這是程序員必須處理的異常,不處理則程序不能通過編譯,比如IOException表示I/O異常,SQLException表示數據庫訪問異常。

我們知道,一個對象的創建要經過內存分配、靜態代碼初始化、構造函數執行等過程,對像生成的關鍵步驟是構造函數,那是不是也允許在構造函數中拋出異常呢?從Java語法上來說,完全可以在構造函數中拋出異常,三類異常都可以,但是從系統設計和開發的角度來分析,則盡量不要在構造函數中拋出異常,我們以三種不同類型的異常來說明之。

(1)構造函數拋出錯誤是程序員無法處理的

在構造函數執行時,若發生了VirtualMachineError虛擬機錯誤,那就沒招了,只能拋出,程序員不能預知此類錯誤的發生,也就不能捕捉處理。

(2)構造函數不應該拋出非受檢異常我們來看這樣一個例子,代碼如下:


class Person{

public Person(int_age){

//不滿18歲的用戶對像不能建立

if(_age<18){

throw new RuntimeException(\"年齡必須大於18歲。\");

}

}

//看限制級的電影

public void seeMovie(){

System.out.println(\"看限制級電影\");

}

}


這段代碼的意圖很明顯,年齡不滿18歲的用戶根本不會生成一個Person實例對象,沒有對象,類行為seeMovie方法就不可執行,想法很好,但這會導致不可預測的結果,比如我們這樣引用Person類。


public static void main(Stringargs){

Person p=new Person(17);

p.seeMovie();

/*其他的邏輯處理*/

}


很顯然,p對像不能建立,因為是一個RuntimeException異常,開發人員可以捕捉也可以不捕捉,代碼看上去邏輯很正確,沒有任何瑕疵,但是事實上,這段程序會拋出異常,無法執行。這段代碼給了我們兩個警示:

加重了上層代碼編寫者的負擔

捕捉這個RuntimeException異常吧,那誰來告訴我有這個異常呢?只有通過文檔來約束了,一旦Person類的構造函數經過重構後再拋出其他非受檢異常,那main方法不用修改也是可以通過測試的,但是這裡就可能會產生隱藏的缺陷,而且還是很難重現的缺陷。

不捕捉這個RuntimeException異常,這是我們通常的想法,既然已經寫成了非受檢異常,main方法的編碼者完全可以不處理這個異常嘛,大不了不執行Person的方法!這是非常危險的,一旦產生異常,整個線程都不再繼續執行,或者連接沒有關閉,或者數據沒有寫入數據庫,或者產生內存異常,這些都是會對整個系統產生影響。

後續代碼不會執行

main方法的實現者原本只是想把p對象的建立作為其代碼邏輯的一部分,執行完seeMovie方法後還需要完成其他邏輯,但是因為沒有對非受檢異常進行捕捉,異常最終會拋出到JVM中,這會導致整個線程執行結束後,後面所有的代碼都不會繼續執行了,這就對業務邏輯產生了致命的影響。

(3)構造函數盡可能不要拋出受檢異常

我們來看下面的例子,代碼如下:


//父類

class Base{

//父類拋出IOException

public Base()throws IOException{

throw new IOException();

}

}

//子類

class Sub extends Base{

//子類拋出Exception異常

public Sub()throws Exception{

}

}


就這麼一段簡單的代碼,展示了在構造函數中拋出受檢異常的三個不利方面:

導致子類代碼膨脹

在我們的例子中子類的無參構造函數不能省略,原因是父類的無參構造函數拋出了IOException異常,子類的無參構造函數默認調用的是父類的構造函數,所以子類的無參構造也必須拋出IOException或其父類。

違背了裡氏替換原則

裡氏替換原則是說「父類能出現的地方子類就可以出現,而且將父類替換為子類也不會產生任何異常」,那我們回過頭來看看Sub類是否可以替換Base類,比如我們的上層代碼是這樣寫的:


public static void main(Stringargs){

try{

Base base=new Base();

}catch(IOException e){

//異常處理

}

}


然後,我們期望把new Base()替換成new Sub(),而且代碼能夠正常編譯和運行。非常可惜,編譯通不過,原因是Sub的構造函數拋出了Exception異常,它比父類的構造函數拋出的異常範圍要寬,必須增加新的catch塊才能解決。

可能有讀者要問了,為什麼Java的構造函數允許子類的構造函數拋出更廣泛的異常類呢?這正好與類方法的異常機制相反,類方法的異常是這樣要求的:


//父類

class Base{

//父類方法拋出Exception

public void method()throws Exception{

}

}

//子類

class Sub extends Base{

//子類方法的異常類型必須是父類方法的子類型

@Override

public void method()throws IOException{

}

}


子類的方法可以拋出多個異常,但都必須是被覆寫方法的子類型,對我們的例子來說,Sub類的method方法拋出的異常必須是Exception的子類或Exception類,這是Java覆寫的要求。構造函數之所以與此相反,是因為構造函數沒有覆寫的概念,只是構造函數間的引用調用而已,所以在構造函數中拋出受檢異常會違背裡氏替換原則,使我們的程序缺乏靈活性。

子類構造函數擴展受限

子類存在的原因就是期望實現並擴展父類的邏輯,但是父類構造函數拋出異常卻會讓子類構造函數的靈活性大大降低,例如我們期望這樣的構造函數。


class Sub extends Base{

public Sub()throws Exception{

try{

super();

}catch(IOException e){

//異常處理後再拋出

throw e;

}finally{

//收尾處理

}

}

}


很不幸,這段代碼編譯通不過,原因是構造函數Sub中沒有把super()放在第一句話中,想把父類的異常重新包裝後再拋出是不可行的(當然,這裡有很多種「曲線」的實現手段,比如重新定義一個方法,然後父子類的構造函數都調用該方法,那麼子類構造函數就可以自由處理異常了),這是Java語法限制。

將以上三種異常類型匯總起來,對於構造函數,錯誤只能拋出,這是程序人員無能為力的事情;非受檢異常不要拋出,拋出了「對己對人」都是有害的;受檢異常盡量不拋出,能用曲線的方式實現就用曲線方式實現,總之一句話:在構造函數中盡可能不出現異常。

注意 在構造函數中不要拋出異常,盡量曲線救國。