java進階——異常

我提到最近一直爲一個項目進行Code Review的工作,從中發現了一些問題,同時也有了一些想法。這次我們來關注一個我們每天都會面對的問題:異常處理。

—異常處理不簡單—

個人覺得,異常處理對於程序員來說,尤其是對於那些初級程序員來說,是最爲熟悉的同時也是最難掌握的。說它熟悉,因爲僅僅就是Try/Catch而已。說它難以掌握,很多開發人員卻說不清楚Try/Catch應該置於何處?什麼情況下需要對異常進行日誌記錄?什麼情況下需要對異常進行封裝?什麼情況下需要對異常進行替換?對於捕獲的異常,在什麼情況下需要將其再次拋出?什麼情況下則不需要。總之,異常處理沒有我們想象的那麼簡單。

無論對於何種類型的應用,異常處理都是必不可少的。合理的異常處理應該是場景驅動的,在不同的場景下,採用的異常處理策略往往是不同的。異常處理的策略應該是可配置的,因爲應用程序出現怎樣的異常往往是不可預測的,現有異常策略的不足往往需要在真正出現某種異常的時候纔會體現出來,所以我們需要一種動態可配置的異常處理策略維護方式。

—try/catch要不要用

1.程序要健壯,必須要設計報錯機制。
最古老,也是最常見的,比如:

bool CreateFile( );
//如果創建文件失敗就返回false,否則返回true。

這種報錯方式,顯然不好。因爲它沒有給出產生錯誤的具體原因。

2.改進:一個函數或過程,會因爲不同的原因產生錯誤,報錯機制必須要把這些錯誤原因進行區分後,再彙報。
比如:

int CreateFile():
//如果創建成功就返回1.
//如果是因爲沒有權限,導致失敗,返回-1。
//如果是因爲文件已經存在,導致失敗,返回-2。
//如果是因爲創建文件發生超時,導致失敗,返回-3。

這樣看上去,比【1】要好些,至少指出了比較具體的失敗原因,但是,還不夠。

3.很多情況下,函數需要把詳細的原因,用字符串的方式,返回:

class Result
{
....int State;//同【2】
....string ErrorMessage;//如果失敗,這裏將給出詳細的信息,如果有可能,應該把建議也寫上去。
}
Result CreateFile();
//如果創建成功,返回的Result,State爲1,ErrorMessage爲null。
//如果是因爲沒有權限,導致失敗,返回的Result,State爲-1,ErrorMessage爲"用戶【guest】沒有權限在【C:\】這個目錄下創建該文件。建議您向管理員申請權限,或者更換具有權限的用戶。"。
//如果是因爲文件已經存在,導致失敗,返回的Result,State爲-2,ErrorMessage爲"文件【C:\abc.txt】已經存在。如果需要覆蓋,請添加參數:arg_overwrite = true"。
//如果是因爲創建文件發生超時,導致失敗,返回的Result,State爲-3,ErrorMessage爲"在創建文件時超時,請使用chkdsk檢查文件系統是否存在問題。"。

4.我個人推崇上面這種方式,完整,美觀。但是這種流程,容易與正常的代碼混在一起,不好區分開。

因此,Java、C#等設計了try catch這一種特殊的方式:

void CreateFile()
//如果創建成功就不會拋出異常。
//如果是因爲沒有權限,導致失敗,會拋出AccessException,這個Exception的Msg屬性爲"用戶【guest】沒有權限在【C:\】這個目錄下創建該文件。建議您向管理員申請權限,或者更換具有權限的用戶。"。
//如果是因爲文件已經存在,導致失敗,會拋出FileExistedException,這個Exception的Msg屬性爲"文件【C:\abc.txt】已經存在。如果需要覆蓋,請添加參數:arg_overwrite = true"。
//如果是因爲創建文件發生超時,導致失敗,會拋出TimeoutException,這個Exception的Msg屬性爲"在創建文件時超時,請使用chkdsk檢查文件系統是否存在問題。"。

可見,上述機制,實際上是用不同的Exception代替了【3】的State。

這種機制,在外層使用時:

try
{
....CreateFile( "C:\abc.txt" );
}
catch( AccessException e )
{
....//代碼進入這裏說明發生【沒有權限錯誤】
}
catch( FileExistedException e )
{
....//代碼進入這裏說明發生【文件已經存在錯誤】
}
catch( TimeoutException e )
{
....//代碼進入這裏說明發生【超時錯誤】
}
對比一下【3】,其實這與【3】本質相同,只是寫法不同而已。

        綜上,我個人喜歡【3】這類面向過程的寫法。但很多喜歡面向對象的朋友,估計更喜歡【4】的寫法。然而【3】與【4】都一樣。這兩種機制都是優秀的錯誤處理機制。

—失敗的異常處理—

try
{
....CreateFile( "C:\abc.txt" );
}
catch( Exception e )
{
....//代碼進入這裏說明發生錯誤
}
當出錯後,只知道它出錯了,並不知道是什麼原因導致錯誤。這同【1】。

以及,即使CreateFile是按【4】的規則設計的,但菜鳥在外層是這樣使用的:

try
{
....CreateFile( "C:\abc.txt" );
}
catch( Exception e )
{
....//代碼進入這裏說明發生錯誤
....throw Exception( "發生錯誤" )
}
這種情況下,如果這位菜鳥的同事,調用了這段代碼,或者用戶看到這個錯誤信息,也只能知道發生了錯誤,但並不清楚錯誤的原因。這與【1】是相同的。

—異常使用的誤區—

第一,對錯誤進行異常處理。所謂錯誤就是,這裏本來不該發生,你卻讓它發生了,比如傳入空指針,數組越界,除數爲零。這種時候正確的做法是加斷言,或者什麼也不做。處理這種錯誤是自作多情而不負責任的,你知道他爲啥錯了你就給他處理?你處理了只是把鴕鳥腦袋埋進沙子,問題依然存在。底層庫沒必要爲上層程序員的邏輯錯誤擦屁股。

第二,認爲異常比返回錯誤碼更安全。異常可以直接跳過多層,它必然不如一層層處理安全。如果不配合自動垃圾回收,跳出多層很難保證內存不泄漏,而有gc的語言也難以避免資源泄露。因此,不建議處理異常直接跳出多層函數。

那麼異常就沒用了嗎?當然不是。異常比錯誤碼能夠攜帶更直觀的信息,高級語言應該使用異常代替錯誤碼,同時不觸及以上誤區,則可使程序更健壯。
—Effective Java—

1、只針對不正常的情況才使用異常

2、不要忽略異常

try {
...
} catch (SomeException e) {
}

—那些情況需要異常處理—

使用者(包括用戶、代碼庫的使用者)所引發的錯誤,需要通過異常機制來處理。

判斷文件先存在,再讀寫文件,其實就是這個問題,按照程序的流程,可以保證在判斷是否存在的時候,文件的存在性,但是不能保證在真正操作文件的時候文件的存在性(例如判斷的時候文件還在,真正操作之前卻被用戶自己刪掉了)。因爲流程上無法對流程外的用戶行爲(用戶刪文件)作出保證,所以需要異常機制。

用戶在程序運行時觸發所導致的錯誤,需要異常機制來捕捉和處理。

程序設計中還有一種叫斷言(ASSERT)的東西,這種機制是用來約束程序設計者的,例如某些庫的某些函數,在文檔中約定了,這個函數的參數必須是>0,那麼你在編程的時候愣是硬生生輸入一個0,那麼這時候就應該選擇斷言,用於幫助程序設計者及早的發現自己程序中的錯誤(這種錯誤是設計上的錯誤所引發的,而非用戶的操作所導致的),而不是用異常機制去處理。
由程序員設計不足所導致的錯誤,需要用斷言來捕捉和處理。

—異常的目的—

將這個問題傳遞給調用方解決。


參考文章:

http://taobaofed.org/blog/2015/10/28/try-catch-runing-problem/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章