什麼是異常,我們爲什麼要關心它
單詞“exception”是短語“exceptional event(異常事件)”的縮寫,它定義如下:
定義:異常是程序在執行時發生的事件,它會打斷指令的正常流程。
許多種類的錯誤將觸發異常,這些問題從像硬盤(crash)墜毀這樣的嚴重硬件錯誤,到嘗試訪問越界數組元素這樣的簡單程序錯誤,像這樣的錯誤如果在java函數中發生,函數將創建一個異常對象並把他拋出到運行時系統(runtime system)。異常對象包含異常的信息,包括異常的類型,異常發生時程序的狀態。運行時系統則有責任找到一些代碼處理這個錯誤。在java技術詞典中,創建一個異常對象並把它拋給運行時系統叫做:拋出異常(throwing an exception)。
當某個函數拋出一個異常後,運行時系統跳入了這樣一個動作,就是找到一些人(譯者注:其實是代碼)來處理這個異常。要找的處理異常的可能的人(代碼)的集合(set)是:在發生異常的方法的調用堆棧(call stack)中的方法的集合(set)。運行時系統向後搜尋調用堆棧,從錯誤發生的函數,一直到找到一個包括合適的異常處理器(exception handler)的函數。一個異常處理器是否合適取決於拋出的異常是否和異常處理器處理的異常是同一種類型。因而異常向後尋找整個調用堆棧,直到一個合格的異常處理器被找到,調用函數處理這個異常。異常處理器的選擇被叫做:捕獲異常(catch the exception)。
如果運行時系統搜尋整個調用堆棧都沒有找到合適的異常處理器,運行時系統將結束,隨之java程序也將結束。
使用異常來管理錯誤,比傳統的錯誤管理技術有如下優勢:
1. 將錯誤處理代碼於正常的代碼分開。
2. 沿着調用堆棧向上傳遞錯誤。
3. 將錯誤分作,並區分錯誤類型。
1. 將錯誤處理代碼於正常的代碼分開。
在傳統的程序種,錯誤偵測,報告,和處理,經常導致令人迷惑的意大利麪條式(spaghetti)的代碼。例如,假設你要寫一個將這個文件讀到內存種的函數,用僞代碼描述,你的函數應該是這個樣子的:
readFile{
open the file; //打開文件
determine its size; //取得文件的大小
allocate that much memory; //分配內存
read the file into memory; //讀文件內容到內存中
close the file; //關閉文件
}
匆匆一看,這個版本是足夠的簡單,但是它忽略了所有潛在的問題:
n 文件不能打開將發生什麼?
n 文件大小不能取得將發生什麼?
n 沒有足夠的內存分配將發生什麼?
n 讀取失敗將發生什麼?
n 文件不能關閉將發生什麼?
爲了在read_file函數中回答這些錯誤,你不得不加大量的代碼進行錯誤偵測,報告和處理,你的函數最後將看起來像這個樣子:
errorCodeType readFile {
initialize errorCode = 0;
open the file;
if (theFileIsOpen) {
determine the length of the file;
if (gotTheFileLength) {
allocate that much memory;
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
close the file;
if (theFileDidntClose && errorCode == 0) {
errorCode = -4;
} else {
errorCode = errorCode and -4;
}
} else {
errorCode = -5;
}
return errorCode;
}
隨着錯誤偵測的建立,你的最初的7行代碼(粗體)已經迅速的膨脹到了29行-幾乎400%的膨脹率。更糟糕的是有這樣的錯誤偵測,報告和錯誤返回值,使得最初有意義的7行代碼淹沒在混亂之中,代碼的邏輯流程也被淹沒。很難回答代碼是否做的正確的事情:如果函數分配內容失敗,文件真的將被關閉嗎?更難確定當你在三個月後再次修改代碼,它是否還能夠正確的執行。許多程序員“解決”這個問題的方法是簡單的忽略它,那樣錯誤將以死機來報告自己。
對於錯誤管理,Java提供一種優雅的解決方案:異常。異常可以使你代碼中的主流程和處理異常情況的代碼分開。如果你用異常代替傳統的錯誤管理技術,readFile函數將像這個樣子:
readFile {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}
注意:異常並不能節省你偵測,報告和處理錯誤的努力。異常提供給你的是:當一些不正常的事情發生時,將所有蹩腳(grungy)的細節,從你的程序主邏輯流程中分開。
另外,異常錯誤管理的膨脹係數大概是250%,比傳統的錯誤處理技術的400%少的多。
2. 沿着調用堆棧向上傳遞錯誤。
異常的第二個優勢是,可以沿着函數的調用堆棧向上報告錯誤。設想,readFile函數是一系列嵌套調用函數中的第四個函數:method1調用method2, method2 調用method3,最後method3調用readFile。
method1 {
call method2;
}
method2 {
call method3;
}
method3 {
call readFile;
}
如果只有method1對發生在readFile中的錯誤感興趣。傳統的錯誤通知技術迫使mothed2和mothed3沿着調用堆棧向上傳遞readFile的錯誤代碼,直到到達對錯誤感興趣的mothed1。
method1 {
errorCodeType error;
error = call method2;
if (error)
doErrorProcessing;
else
proceed;
}
errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
proceed;
}
errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
proceed;
}
就像前面所瞭解到的,java運行時系統向後(baskward,也就是向上)搜尋調用堆棧,找到對處理這個異常感興趣的函數。Java函數可以“躲開(duck)”任何在函數中拋出的異常,因此,允許函數穿越過調用堆棧捕獲它。那個唯一對錯誤感興趣的函數method1,將負責(worry about)偵測錯誤。
method1 {
try {
call method2;
} catch (exception) {
doErrorProcessing;
}
}
method2 throws exception {
call method3;
}
method3 throws exception {
call readFile;
}
然而,就像你在僞代碼中看到的,在“中間人(中間的函數methoed2和method3)”忽略異常需要受到影響,一個函數如果要拋出一個異常,必須在函數的公共接口聲明中使用throws關鍵字指定。因此,一個函數可以通知它的調用者自己會拋出什麼樣的異常,這樣調用者就可以有意識的決定對這些異常做些什麼。
再一次注意異常和傳統錯誤處理方式,在膨脹係數和迷惑係數的區別。使用異常的代碼更簡潔,更容易理解。
3. 將錯誤分作,並區分錯誤類型。
異常(們)經常被劃分成類別或組。例如,你可以想象一組異常,它們中每一個都表示關於數組操作的的特殊的異常:索引超出數組的範圍,要插入的元素是錯誤的類型,要查找的元素不在數組中。而且,你能想象一些函數將處理所有這類的異常(關於數組的異常),其它一些函數將處理特殊異常(僅僅是無效索引異常)。
由於在java程序中所有的異常首先是一個對象,異常的分組和分類成爲類繼承自然而然的結果。Java異常必須是Throwable或者是Throwable子類的實例。就像你可以從其它java類繼承一樣,你也可以創建Thowable的子類,或者孫子類(從Thowable子類繼承)。每個葉子節點(沒有子類的類),代表一種特殊類型的異常,每個節點(node)(有一個或者更多子類的類)代表一組有關聯的異常。
例如,在下列圖表中,ArrayException是Exception的子類(Throwable的一個子類),它有三個子類。
InvalidIndexException, ElementTypeException,和NoSuchElementException都是葉子類,它們都是在操作數組時發生的錯誤。捕獲異常的一種方法是僅僅捕捉那些葉子類的實例。例如,一個異常控制器僅僅捕捉無效索引異常,它的代碼像這個樣子:
catcatch (InvalidIndexException e) {
. . .
}
ArrayExecption是節點(node)類,代表你在操作數組時發生的任何錯誤,包括特定代表一個錯誤的所有子類中的任何一個。如果一個函數要一組或者一類異常,只要在在cathc語句中指定這些異常的超類(superclass)。例如,要捕捉所有數組異常而不指定具體類型,異常控制器將捕捉ArrayException:
catch (ArrayException e) {
. . .
}
這個異常控制器將捕捉所有數組異常,包括InvalidIndexException, ElementTypeException, 和 NoSuchElementException。你可以在異常控制器的參數e中找到異常的精確的類型。你甚至可以建立這樣的異常控制器,它處理所有的Exception。
catch (Exception e) {
. . .
}
上面出示的異常控制器實在時太通用了,這樣做使你的代碼處理太多的錯誤,需要處理許多你不希望處理的異常,因而不能正確的處理異常。通常我們不推薦寫通用的異常處理器。
就像你看到的,你能創建一組異常,並處理這些異常以通用的方式;你也能指定異常類型去區分異常,並處理異常以精確的方式。