軟件開發中一個古老的說法是:80%的工作使用20%的時間。80%是指檢查和處理錯誤所付出的努力。在許多語言中,編寫檢查和處理錯誤的程序代碼很乏味,并使應用程序代碼變得冗長。原因之一就是它們的錯誤處理方式不是語言的一部分。盡管如此,錯誤檢測和處理仍然是任何健壯應用程序最重要的組成部分。
Java提供了一種很好的機制,用強制規定的形式來消除錯誤處理過程中隨心所欲的因素:異常處理。它的優秀之處在于不用編寫特殊代碼檢測返回值就能很容易地檢測錯誤。而且它讓我們把異常處理代碼明確地與異常產生代碼分開,代碼變得更有條理。異常處理也是Java中唯一正式的錯誤報告機制。
第一部分 異常
1、拋出異常。所有的標準異常類都有兩個構造器:一個是缺省構造器,一個是帶參數的構造器,以便把相關信息放入異常對象中。
throw new NullPointerException();
throw new NullPointerException("t = null");
2、如果有一個或者多個catch塊,則它們必須緊跟在try塊之后,而且這些catch塊必須互相緊跟著,不能有其他任何代碼。C++沒有這樣的限制,所以C++的異常處理處理不好就會寫得很亂,拋來拋去的。
3、使用try塊把可能出現異常的代碼包含在其中,這么做的好處是:處理某種指定的異常的代碼,只需編寫一次。作業沒寫完的同學到走廊罰站去,這符合我們處理問題的方式,不用挨個地告訴。
4、無論是否拋出異常,finally塊封裝的代碼總能夠在try塊之后的某點執行。
例子:
try {
return ;
}
finally{
System.out.print("You can't jump out of my hand!");
}
甚至你在try塊內用return語句想跳過去都不可以!finally內的輸出語句還是執行了!別想逃出我的手掌心!
5、catch塊和finally塊是可選的,你可以只使用try。但是這么做有意思嗎?
6、推卸責任。Java允許你推卸責任,沒有必要從相應的try塊為每個可能的異常都編寫catch子句。Java2類庫中很多方法都會拋出異常,就是是把異常處理的權限交給了我們用戶。畢竟,Java不知道你的自行車被偷了之后,你會去報案還是會忍氣吞聲自認倒霉,或者偷別人的自行車。我們需要這種處理異常的自由度。
7、調用棧。調用棧是程序執行以訪問當前方法的方法鏈。被調用的最后一個方法在棧的頂部,它將被最先執行完畢,然后彈出;第一個調用方法位于底部,也就是main函數。在catch子句中使用printStackTrace()方法打調用棧信息是比較常用的定位異常的方法。printStackTrace()繼承自Throwable。
8、異常的傳播。在一個方法A中,如果一個異常沒有得到處理,它就會被自動拋到調用A方法的B方法中。如果B方法也沒有處理這個異常,他就會被繼續依次向上拋,直到main方法。如果main也沒有理會它,那么異常將導致JVM停止,程序就中止了。你被同學揍了,先去告訴老師。老師不理你你就去告訴教導處主任,教導處主任也不管那只能告訴校長,校長還不管!沒有比他更大的了,于是你崩潰了,學業中止了……下面這段程序記錄了悲慘的輟學歷史:
class ExceptionDemo {
static void student() throws Exception{
teacher();
}
static void teacher() throws Exception{
schoolmaster();
}
static void schoolmaster() throws Exception{
throw new Exception();
}
public static void main(String[] args) {
try {
student();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
輸出結果是:
java.lang.Exception
at ExceptionDemo.schoolmaster(ExceptionDemo.java:9)
at ExceptionDemo.teacher(ExceptionDemo.java:6)
at ExceptionDemo.student(ExceptionDemo.java:3)
at ExceptionDemo.main(ExceptionDemo.java:13)
可以看出函數的調用棧,一級一級地哭訴……
9、異常的層次結構及Error。
Object
Throwable
Error Exception
Throwable繼承自Object,Error和Exception繼承自Throwable。Error比較特殊,它對應于我們常說的不可抗拒的外力,房屋中介的合同上總有一條,如遇不可抗拒的外力本合同中止,返還乙方押金。我不安地問:不可抗拒的外力指什么?中介回答:比如戰爭、彗星撞擊地球等。對Java來說Error是指JVM內存耗盡等這類不是程序錯誤或者其他事情引起的特殊情況。一般地,程序不能從Error中恢復,因此你可以能眼睜睜地看著程序崩潰而不必責怪自己。嚴格來講,Error不是異常,因為它不是繼承自Exception。
10、誰之錯?一般地,異常不是我們程序員的錯,不是程序設計上的缺陷。比如讀取一個重要文件,這個文件被用戶誤刪了;正上著網呢,網線被用戶的寵物咬斷了。為了程序的健壯性,我們盡量考慮出現可能性大的異常,并處理,但我們不能窮盡。
11、異常的捕獲之一。catch子句的參數是某種類型異常的對象,如果拋出的異常是該參數的子類,那么這個異常將被它捕獲。也就是說被拋出的異常不會精確地尋找最匹配的捕獲者(catch子句),只要是它的繼承結構的直系上層就可以捕獲它。
按照這個邏輯,catch(Exception e) 不就能捕獲所有的異常嗎?事實上,確實如此。 但是一般地,不建議使用這種一站式的異常處理。因為這樣就丟失了具體的異常信息,不能為某個具體的異常編寫相應的異常處理代碼,失去了異常處理的意義。從哲學角度來講,具體問題要具體分析,能治百病的萬能藥一般都是無效的保健品。
Java在此處為什么這么設計呢?因為有另一種機制的存在,請看下條分解。
12、異常的捕獲之二。當拋出一個異常時,Java試圖尋找一個能捕獲它的catch子句,如果沒找到就會沿著棧向下傳播。這個過程就是異常匹配。Java規定:最具體的異常處理程序必須總是放在更普通異常處理程序的前面。這條規定再合理不過了,試想如果把catch(Exception e)放在最上面,那么下面的catch子句豈不是永遠不能執行了?如果你非要把更普遍的異常處理放在前面,對不起,通不過編譯!雖然編譯器不會這樣報錯:“It is so stupid to do like that!”……
13、捕獲或聲明規則。如果在一個方法中拋出異常,你有兩個選擇:要么用catch子句捕獲所有的異常,要么在方法中聲明將要拋出的異常,否則編譯器不會讓你得逞的。
方案一:處理異常
void ex(){
try{
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
方案二:拋出去
void ex() throws Exception{
throw new Exception();
}
比較一下行數就知道了,在代碼的世界里推卸責任也是那么簡單,一個throws關鍵字包含了多少人生哲理啊……現實生活中我們有很多角色,兒女、父母、學生、老師、老板、員工……每個人都占了幾條。可是你能盡到所有責任嗎?按照古代的孝道,父母尚在人世就不能遠行。各種責任是有矛盾的,顧此失彼啊。
但是這條規則有個特例。一個繼承自Exception名為RuntimeException的子類,也就是運行時異常,不受上述規則的限制。下面的代碼完全能編譯,只不過調用之后在運行時會拋出異常。
void ex(){
throw new RuntimeException();
}
14、throw和thrwos關鍵字。throw用在方法體中拋出異常,后面是一個具體的異常對象。throws用在方法參數列表括號的后面,用來聲明此方法會拋出的異常種類,后面跟著一個異常類。
15、非檢查異常。RuntimeException、Error以及它們的子類都是非檢查異常,不要求定義或處理非檢查異常。Java2類庫中有很多方法拋出檢查異常,因此會常常編寫異常處理程序來處理不是你編寫的方法產生的異常。這種機制強制開發人員處理錯誤,使得Java程序更加健壯,安全。
16、自定義異常類型。覺得現有的異常無法描述你想拋出的異常,ok!Java允許你自定義異常類型,只需要繼承Exception或者它的子類,然后換上有個性的名字。
class NotEnoughMoney extends Exception {
public NotEnoughMoney() {}
public NotEnoughMoney(String msg) { super(msg); }
}
希望大家在生活里不要拋出類似的異常。
17、重新拋出異常。一個很無聊的話題,純粹的語法研究,實際意義不大。當catch子句捕獲到異常之后可以重新拋出,那么它所在的方法必須聲明該異常。
void ex() throws Exception{
try {
throw new Exception();
}
catch (Exception mex) {
throw me;
}
}
18、異常處理機制的效率。待補充……
19、終止與恢復模型。異常處理理論上有兩種模型:
一、終止模型。錯誤很關鍵且無法挽回,再執行下去也沒意義,只能中止。“羅密歐,我們分手吧!”“好吧,朱麗葉!”
二、恢復模型。經過錯誤修正重新嘗試調用原來出問題的方法。“羅密歐,我們分手吧!”“朱麗葉,我錯了!請再原諒我一次吧!”“好的,再原諒你最后一次!”
顯然我們更喜歡恢復模型,但在實際中,這種模式是不易實現和維護的。
例子:用戶輸入了非法的字符,分別按照兩種模式處理
一、終止模型。輸出出錯信息而已,一旦用戶手一抖眼一花你的代碼就崩潰了
double number;
String sNumber = "";
try {
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
sNumber = bf.readLine();
number = Double.parseDouble(sNumber);
} catch (IOException ioe) {
System.err.println("some IOException");
} catch (NumberFormatException nfe) {
System.err.println(sNumber + " is Not a legal number!");
}
//System.out.println(number);
二、恢復模型。小樣!不輸入正確的數據類型就別想離開!
double number = 0;
String sNumber = "";
while(true){
try {
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
sNumber = bf.readLine();
number = Double.parseDouble(sNumber);
break; //如果代碼能執行到這一行,就說明沒有拋出異常
} catch (IOException ioe) {
System.err.println("some IOException");
} catch (NumberFormatException nfe) {
System.err.println(sNumber + " is Not a legal number!");
}
}
System.out.println(number);
直到用戶輸入正確的信息才會被該代碼放過。這是一種簡單的恢復模型的實現,挺耐看的,我很喜歡!
20、try、catch、finally內變量的作用域和可見性。
在try塊內定義的變量,它在catch或者finally塊內都是無法訪問到的,并且在整個異常處理語句之外也是不可見的。
補充一點初始化:第一個例中最后一句被注釋掉了。number是在運行時由用戶輸入而初始化的,但是在編譯時刻并沒有初始化,編譯器會抱怨的。
21、輸出異常信息。捕捉到異常之后,通常我們會輸出相關的信息,以便更加明確異常。
catch (Exception mex) {
System.err.println("caught a exception!");
}
用標準錯誤流System.err比System.out要好。因為System.out也許會被重定向,System.err則不會。
22、更高級的話題我會補充上的,但是我的肚子拋出了Hungry異常,我必須catch然后調用eat()方法補充能量。昨晚的魷魚蓋澆飯很好吃……
芳兒寶貝.我愛你