1、背景介紹
為什麼要進行異常處理?
對於電腦程式而言,沒有人能保證它在運行時不會出錯,出錯來源主要有以下幾個:
程式出現錯誤,那麼該如何解決呢?在Java語言中,它提供了異常處理機制來協助我們處理這個問題。
異常機制可以使程式中的異常處理代碼和正常業務代碼分離開,保證程式碼更加優雅,並可以提高程式的健壯性。
Java的異常機制主要依賴於try、catch、finally、throw和throws五個關鍵字。
try關鍵字後緊跟一個花括弧括起來的代碼塊(花括弧不可省略),簡稱try塊,它裡面放置可能引發異常的代碼;catch後對應異常類型和一個代碼塊,用於表明該catch塊用於處理這種類型的代碼塊;多個catch塊後還可以跟一個finally塊,finally塊用於回收在try塊裡開啟的實體資源,異常機制會保證finally塊總被執行;throws關鍵字主要在方法簽名中使用,用於聲明該方法可能拋出的異常;throw用於拋出一個實際的異常,throw可以單獨作為語句使用,拋出一個具體的異常對象。
簡單例子如下
try { // args表示傳入的形參,args[0]表示傳入的第一個形參,args[1]表示傳入的第二個形參 int a = Integer.parseInt(args[0]); int b = Integer.parseInt(args[1]); int c = a / b; System.out.println("您輸入的兩個數相除的結果是:"+c);} catch (IndexOutOfBoundsException ie){ System.out.println("數組越界:運行程式時輸入的參數個數不夠");} catch (NumberFormatException ne){ System.out.println("數字格式異常:程式只能接收整數參數");} catch (ArithmeticException ae){ // 除0異常 System.out.println("算術異常");} catch (Exception e){ System.out.println("未知異常");}
2、知識剖析
2.1 異常處理機制
2.1.1 使用try...catch捕獲異常
try{
// 業務實現代碼
} catch (Exception e){
// 異常處理代碼
}
執行流程(或者說邏輯):
如果try塊裡代碼能夠順利運行,那就“一切正常”;如果執行try塊裡的商務邏輯代碼時出現異常,系統會自動產生一個異常對象,該異常對象被提交給Java運行時環境 ,這個過程被稱為拋出(throw)異常。當Java運行時環境收到異常對象後,會尋找能處理該異常對象的catch塊,如果找到合適的catch塊,則把該異常對象交給該catch塊處理,這個過程被稱為捕獲(catch)異常;如果Java運行時環境找不到捕獲異常的catch塊,則運行時環境終止,Java程式也將退出。
註:不管程式碼塊是否處於try塊中,甚至包括catch塊中的代碼,只要執行該代碼塊時出現了異常,系統總會自動產生一個異常對象。如果程式沒有為這段代碼定義任何的catch塊,則Java運行時環境無法找到處理該異常的catch塊,程式就此退出。
2.1.2 異常類的繼承體系
當Java運行時環境接收到異常對象後,會一次判斷該異常對象是否是catch塊後異常類或其子類的執行個體,如果是,Java運行時環境將調用該catch塊來處理該異常,否則再次拿該異常對象和下一個catch塊裡的異常類進行比較。
Java異常捕獲流程
可以看出,try塊後可以有多個catch塊,這是為了針對不同的異常類提供不同的異常處理方式。當系統發生不同的意外情況時,系統會產生不同的異常對象,Java運行時就會根據該異常對象所屬的異常類來決定使用哪個catch塊來處理該異常。
在通常情況下,如果try塊被執行一次,則try塊後只有一個catch塊會被執行,絕不可能有多個catch塊被執行。除非在迴圈中使用了continue開始下一次迴圈,下一次迴圈又重新運行了try塊,這才可能導致多個catch塊被執行。
Java常見的異常類之間的繼承關係
Java把所有的非正常情況分為兩種:異常和錯誤,它們都繼承Throwable父類。
Error錯誤,一般指與虛擬機器相關的問題,如系統崩潰、虛擬機器錯誤、動態連結失敗、資源耗盡 等,這種錯誤無法恢複或不可能捕獲,將導致應用程式中斷。通常應用程式無法處理這些錯誤,因此應用程式不應該試圖使用catch塊來捕獲Error對象,也無須聲明可能拋出Error及其任何子類對象。
常見異常:
IndexOutOfBoundsException:數組越界異常,原因在於運行程式時輸入的參數個數不夠
NumberFormatException:數字格式異常,原因在於運行程式時輸入的參數不是數字,而是字母
ArithmeticException:除0異常
Exception:當發生未知異常時,該異常對象總是Exception類或其子類的執行個體,可用Exception對應的catch塊處理該異常
NullPointerException:null 指標異常,當試圖調用一個null對象的執行個體或者執行個體變數時,會引發此異常
註:進行異常捕獲時不僅應該把Exception類對應的catch塊放在最後,而且所有父類異常的catch塊都應該排在子類異常catch塊的後面,即先捕獲小異常,再捕獲大異常,否則將出現編譯錯誤。(若父類在前,則排在它後面的子類的catch塊將永遠不會獲得執行的機會,因為檢索catch塊是從上到下依次執行的)
2.1.3 Java7提供多異常捕獲
在Java7之前,每個catch塊只能捕獲一種類型的異常,但從Java7開始,一個catch塊可以捕獲多種類型的異常。
使用一個catch塊捕獲多種類型的異常時需要注意如下兩個地方:
try { int a = Integer.parseInt(args[0]); int b = Integer.parseInt(args[1]); int c = a / b; System.out.println("您輸入的兩個數相除的結果是:"+c); } catch (IndexOutOfBoundsException|NumberFormatException|ArithmeticException e){ System.out.println("程式發生了數組越界、數字格式異常、算術異常之一"); // 捕獲多異常時,異常變數預設有final修飾 // 所以下面代碼編譯報錯// e = new ArithmeticException("text"); } catch (Exception e){ System.out.println("未知錯誤"); // 捕獲一種類型的異常時,異常變數沒有final修飾 // 所有下面代碼完全正確 e = new RuntimeException("test"); }
2.1.4 訪問異常資訊
如果程式需要在catch塊中訪問異常對象的相關資訊,則可以通過訪問catch後面括弧中的異常形參來獲得。當Java運行時決定調用某個catch塊來處理該異常對象時,會將異常對象賦給catch塊後的異常參數,程式即可通過該參數來獲得異常的相關資訊。
所有的異常對象都包含了如下幾個常用方法:
getMessage():返回該異常的詳細描述字串。
printStackTrace():將該異常的跟蹤棧資訊輸出到標準錯誤流。
printStackTrace(PrintStream s):將該異常的跟蹤棧資訊輸出到指定輸出資料流。
getStackTrace():返回該異常的跟蹤棧資訊。
2.1.5 使用finally
有時,程式在try塊裡開啟了一些實體資源(例如資料庫連接、網路連接和磁碟檔案等),這些實體資源都必須顯示回收。
註:Java的記憶體回收機制不會回收任何實體資源,記憶體回收機制只能回收堆記憶體中對象所佔用的記憶體。
為了保證一定能回收try塊中開啟的實體資源,異常處理機制提供了finally塊。不管try塊中的代碼是否出現異常,也不管哪一個catch塊是否被執行,甚至在try塊或者catch塊中執行了return語句,finally塊總會被執行。
完整的Java異常處理文法結構:
try{
// 業務實現代碼
} catch (Exception1 e){
// 異常處理塊1
} catch (Exception2 e){
// 異常處理塊2
} finally {
// 資源回收塊
}
異常處理文法結構中只有try塊是必需的,如果沒有try塊,則不能有後面的catch塊和finally塊;catch塊和finally塊都是可選的,但catch塊和finally塊至少出現其中之一,也可以同時出現;可以有多個catch塊,捕獲父類異常的catch塊必須位於捕獲子類異常的後面;但不能只有try塊,沒有catch塊,也沒有finally塊;多個catch塊必須位於try塊之後,finally塊必須位於所有catch塊之後。
註:除非在try塊、catch塊中調用了退出虛擬機器的方法,否則不管在try塊、catch塊中執行怎樣的代碼,出現怎樣的情況,異常處理的finally塊總會被執行。
在通常情況下,盡量避免在finally塊中使用return或throw等導致方法終止的語句,否則可能出現一些很奇怪的情況。
2.1.6 異常處理的嵌套
在try塊、catch塊或finally塊中包含完整的異常處理流程的情形被稱為異常處理的嵌套。
異常處理流程代碼可以放在任何能放可執行性代碼的地方,因此完整的異常處理流程既可放在try塊裡,也可放在catch塊裡,還可放在finally塊裡。
異常處理嵌套的深度沒有很明顯的限制,但通常沒有必要使用超過兩層的嵌套異常處理,層次太深一是沒必要,而是導致程式可讀性降低。
2.1.7 Java7的自動關閉資源的try語句
Java7增強了try語句的功能,它允許在try關鍵字後緊跟一對圓括弧,圓括弧可以聲明、初始化一個或多個資源,此處的資源指的是那些必須在程式結束時顯示關閉的資源(比如資料庫連接、網路連接等),try語句在該語句結束時自動關閉這些資源。
註:為了保證try語句可以正常關閉資源,這些資源實作類別必須實現AutoCloseable或Closeable介面,實現這兩個介面就必須實現close()方法。(Closeable是AutoCloseable的子介面;Closeable介面裡的close()方法聲明拋出了IOException,因此它的實作類別在實現close()方法時只能聲明拋出IOException或其子類;AutoCloseable介面裡的close()方法聲明拋出了Exception,因此它的實作類別在實現close()方法時可以聲明拋出任何異常)
自動關閉資源的try語句相當於包含了隱式的finally塊(這個finally塊用於關閉資源),因此這個try語句可以既沒catch塊,也沒有finally塊。如果程式需要,自動關閉資源的try語句後也可以帶多個catch塊和一個finally塊。
Java7幾乎把所有的“資源類”(包括檔案的IO的各種類、JDBC編程的Connection、Statement等介面)進行了改寫,改寫後的資源類都實現了AutoCloseable或Closeable介面。
2.2 Checked異常和Runtime異常體系
Java的異常被分為兩大類:Checked異常和Runtime異常(運行時異常,有的也稱未檢查異常)。所有的RuntimeException類及其子類的執行個體被稱為Runtime異常;不是RuntimeException類及其子類的異常執行個體則被稱為Checked異常。(在使用時要分辨是什麼類型異常,只需看聲明的異常類是啥就知道了)
只有Java語言提供了Checked異常,其他語言都沒有提供Checked異常。Java認為Checked異常都是可以被處理(修複)的異常,所以Java程式必須顯示處理Checked異常,如果程式沒有處理Checked異常,該程式在編譯時間會發生錯誤,無法通過編譯。
對於Checked異常的處理方式有以下兩種:
Runtime異常則更加靈活,Runtime異常無須顯示聲明拋出,如果程式需要捕獲Runtime異常,也可以使用try...catch塊來實現。
註:Java核心技術
在Exception階層中,有兩個分支:一個分支派生於RuntimeException;另一個分支包含其他異常。劃分這兩個分支的規則是:由程式錯誤導致的異常屬於RuntimeException;而程式本身沒有問題,但由於像I/O錯誤這類問題導致的異常屬於其他異常。
Java語言規範將派生於Error類或RuntimeException類的所有異常稱為未(不受?)檢查異常(unchecked),所有其他的異常稱為已檢查異常(checked)。(在編譯期間進行檢查,編譯器將檢查是否為所有的已檢查異常提供了異常處理器) ??????????? 有待討論
2.3 使用throws拋出異常
使用throws聲明拋出異常的思路是,當前方法不知道如何處理這種類型的異常,該異常應該由上一級調用者處理;如果main方法也不知道如何處理這種類型的異常,也可以使用throws聲明拋出異常,該異常將交給JVM處理。JVM對異常的處理方法是,列印異常的跟蹤棧資訊,並中止程式運行,這就是平時我們的程式在遇到異常後自動結束的原因。
throws聲明拋出只能在方法簽名中使用,throws可以聲明多個異常類,多個異常類之間使用逗號隔開。
一旦使用throws語句聲明拋出異常,那麼就無需使用try...catch塊來捕獲該異常了。
使用throws聲明拋出異常時有一個限制,就是方法重寫時“兩下”中的一條規則:子類方法聲明拋出的異常類型應該是父類方法聲明拋出的異常類型的子類或相同,子類方法聲明拋出的異常不允許比父類方法聲明拋出的異常多。(即設定了上限)
使用Checked異常至少存在如下兩大不便之處:
在大部分時候推薦使用Runtime異常,而不使用Checked異常,尤其當程式需要自行拋出異常時,使用Runtime異常將更加簡潔。
當使用Runtime異常時,程式無需在方法中聲明拋出Checked異常,一旦發生了自訂錯誤,程式只管拋出Runtime異常即可。對於Runtime異常,Java編譯器不要求必須進行異常捕獲處理或者拋出聲明,由程式員自行決定。
使用Runtime異常省事,但Checked異常也有其優勢,Checked異常能在編譯時間提醒程式員代碼可能存在的問題,提醒程式員必須注意處理該異常,或者聲明該異常有方法的調用者來處理,從而避免程式員因為粗心而忘記處理該異常的錯誤。
2.4 使用throw拋出異常
當程式出現錯誤時,系統會自動拋出異常,除此之外,Java也允許程式自行拋出異常,自行拋出異常使用throw語句來完成。
2.5 自訂異常類
使用者自訂異常都應該繼承Exception基類,如果希望自訂Runtime異常,則應該繼承RuntimeException基類。定義異常類時通常需要提供兩個構造器:一個是無參數的構造器;另一個是帶一個字串參數的構造器,這個字串將作為該異常對象的描述資訊(即異常對象的getMessage()方法的傳回值)。
package AuctionException;// 自訂異常都要繼承Exception基類,如果希望自訂Runtime異常,則應該繼承RuntimeException基類。// 自訂異常類需要提供兩個構造器:一個是無參數的構造器;另一個是帶一個字串參數的構造器,這個字串作為// 該異常對象的描述資訊(即異常對象的getMessage()方法的傳回值)public class AuctionException extends Exception{ //1、無參數的構造器 public AuctionException(){ } //2、帶一個字串參數的構造器 public AuctionException(String msg){ // 通過super調用父類的構造器 super(msg); }}
2.6 catch和throw同時使用
在實際應用中,當一個異常出現時,單靠某個方法無法完全處理該異常,必須由幾個方法協作才可以完全處理該異常。也就是說,在異常出現的當前方法中,程式只對異常進行部分處理,還有些處理需要在該方法的調用者中才能完成,所以應該再次拋出異常,讓該方法的調用者也能捕獲到異常。
為了實現這種通過多個方法協作處理同一個異常的情形,可以在catch塊中結合throw語句來完成。
這種catch和throw結合使用使用的情況在大型企業級應用中非常常用,企業級應用對異常的處理通常分成兩個部分:一是後台需要通過日誌來記錄異常發生的詳細情況;二是應用還需要根據異常嚮應用使用者傳達某種提示。
package CatchAndThrow;import AuctionException.AuctionException;public class CatchAndThrow { private double initPrice = 30.0; // 因為該方法中顯示拋出了AuctionException異常 // 所以此處需要聲明拋出AuctionException異常 public void bid(String bidPrice) throws AuctionException{ double d = 0.0; try { d = Double.parseDouble(bidPrice); } catch (Exception e){ // 此處完成本方法中可以對異常執行的修複處理 // 此處僅僅是控制台列印異常的跟蹤棧資訊 e.printStackTrace(); // 再次拋出自訂異常 throw new AuctionException("競拍價必須是數值,不能包含其他字元"); } if (initPrice > d){ throw new AuctionException("競拍價比起拍價低,不允許競拍"); } initPrice = d; } public static void main(String[] args){ CatchAndThrow catchAndThrow = new CatchAndThrow(); try{ catchAndThrow.bid("df"); } catch (AuctionException ae){ // 再次捕獲到bid()方法中的異常,並對該異常進行處理,此處是將異常的詳細描述資訊輸出到標準錯誤(err)輸出 System.err.println(ae.getMessage()); } }}
2.7 異常跟蹤棧
異常對象的printStackTrace()方法用於列印異常的跟蹤棧資訊,根據printStackTrace()方法的輸出結果,開發人員可以找到異常的源頭,並跟蹤到異常一路觸發的過程。
3、常見問題
3.1 為什麼在finally塊中不能訪問try塊中聲明的變數?
答:try塊裡聲明的變數是代碼塊內局部變數,它只在try塊內有效,在catch塊及finally塊中不能訪問該變數。
3.2 為什麼最好不要使用catch all語句?
答:所謂catch all語句指的是一種異常捕獲模組,它可以處理常式發生的所有可能異常。例如使用Exception或者Throwable類捕獲所有異常,雖然這種方式能夠處理異常,但是它不能精確描述引發異常的原因,我們很難進行準確的排查。
3.3 有沒有關於註解方式的異常處理?
答:有的,比如spring有基於註解的全域異常處理方式(使用@ExceptionHandler)
4、參考文獻
摘自《瘋狂Java講義 》第三版,文中代碼模仿書中例子所敲。
今天的分享就到這了,希望大家多多指正,互相學習~