異常簡介
在程序運行過程中出現錯誤,導致程序出現非預期場景。異常處理可以保證出現錯誤后,控制接下來的程序流程,是選擇定位錯誤信息,還是拋出異常或捕獲異常、還是避免程序非正常退出,都取決於我們。
Java的異常體系結構(來自網絡)

Java把異常作為一種類,當做對象來處理。所有異常類的基類是Throwable類,兩大子類分別是Error和Exception。這些異常類可以分為三種類型:系統錯誤、異常和運行時異常。系統錯誤由Java虛擬機拋出,用Error類表示。Error類描述的是內部系統錯誤,例如Java虛擬機崩潰。這種情況僅憑程序自身是無法處理的,在程序中也不會對Error異常進行捕捉和拋出。
異常(Exception)又分為RuntimeException(運行時異常)和CheckedException(檢查異常),兩者區別如下:
- RuntimeException:程序運行過程中出現錯誤,才會被檢查的異常。例如:類型錯誤轉換,數組下標訪問越界,空指針異常、找不到指定類等等。
- CheckedException:來自於Exception且非運行時異常都是檢查異常,編譯器會強制檢查並通過try-catch塊來對其捕獲,或者在方法頭聲明該異常,交給調用者處理。
兩種異常的處理方式:若是運行時異常,則表明程序出錯,應該找到錯誤並修改,而不是對其捕獲。若是檢查異常,遵循該原則:誰知情誰處理,誰負責誰處理,誰導致誰處理。處理就是對其捕獲並處理。
深入了解異常處理
異常處理的5個關鍵字:try、catch、throw、throws和finally。關於它們的用法和注意點,會在下面一一介紹。
Java的異常處理模型基於三種操作: 聲明異常、拋出一個異常和捕獲一個異常。
聲明異常(throws)
//不捕獲,而是聲明該異常,交給調用者處理 public static void method() { public static void method2() throws Exception { /*try-catch塊捕獲異常*/ if(5 > 3) { if(5 > 3) { throw new Exception(); //拋出異常 try { } throw new Exception(); //拋出異常 } } catch (Exception e) { e.printStackTrace();//捕獲異常后的處理 } } }
在Java中,當前執行的語句必屬於某個方法,Java解釋器調用main方法執行開始執行程序。若方法中存在檢查異常,如果不對其捕獲,那必須在方法頭中顯式聲明該異常,以便於告知方法調用者此方法有異常,需要進行處理。 在方法中聲明一個異常,方法頭中使用關鍵字throws,后面接上要聲明的異常。若聲明多個異常,則使用逗號分割。如下所示:
public static void method() throws IOException, FileNotFoundException{ //something statements }
【注意】若是父類的方法沒有聲明異常,則子類繼承方法后,也不能聲明異常。
拋出異常(throw)
如果代碼可能會引發某種錯誤,可以創建一個合適的異常類實例並拋出它,這就是拋出異常。如下所示:
public static double method(int value) { if(value == 0) { throw new ArithmeticException("參數不能為0"); //拋出一個運行時異常 } return 5.0 / value; }
大部分情況下都不需要手動拋出異常,因為Java的大部分方法要么已經處理異常,要么已聲明異常。所以一般都是捕獲異常或者再往上拋。
捕獲異常(try-catch)
當拋出一個異常時,可以在try-catch塊中捕獲它並進行處理。
try { //包含可能會出現異常的代碼以及聲明異常的方法 } catch (ClassCastException e) { //捕獲指定異常並進行處理 }catch(Exception ex) { //捕獲指定異常並進行處理 }
若執行try塊的過程中沒有發生異常,則跳過catch子句。若是出現異常,try塊中剩余語句不再執行。開始逐步檢查catch塊,判斷catch塊的異常類實例是否是捕獲的異常類型。匹配后執行相應的catch塊中的代碼。如果異常沒有在當前的方法中被捕獲,就會被傳遞給該方法的調用者。這個過程一直重復,直到異常被捕獲或被傳給main方法(交給JVM來捕獲)。
catch捕獲異常的順序
一個通用父類可以派生出各種異常類,如果一個catch塊可以捕獲一個父類的異常對象,它就能捕獲那個父類的所有子類的異常對象。如果捕獲的是多個同類型異常,則子類異常在前,父類異常在后,不然會導致編譯錯誤。這是因為父類異常囊括了子類異常,如果父類異常在前,子類異常永遠捕獲不到,導致有時候無法准確描述錯誤信息。
try { File f =new File("C:\\ProgramFile\\test.txt"); FileInputStream fis = new FileInputStream(f); } catch (FileNotFoundException e) { //子類異常 e.printStackTrace(); } catch(IOException ie) { //父類異常 ie.printStackTrace(); } catch(Exception e) { //基類運行時異常 e.printStackTrace(); }
這里只是用於演示catch塊捕獲的順序。捕獲多個異常時,可以使用catch(Exception1 | Exception2| Exception3)的形式來優化捕獲異常的代碼結構。
將聲明異常、拋出異常和捕獲異常綜合在一起。演示如下:
public static void main(String[] args) { for(int i = 2; i < 100; i++) { //對運行時異常,可以選擇捕獲也可以不選擇捕獲 if(isPrime(i)) { System.out.print(i + " "); } } } //檢測是否為質數 public static boolean isPrime(int num) throws IllegalArgumentException{ //拋出一個運行時異常 if(num < 2) throw new IllegalArgumentException("質數不能小於2"); for(int i = 2; i < num; i++) { if(num % i == 0) {//若能被1和本身之外的數整除,則非質數 return false; } } return true; }
因為拋出的是運行時異常,可以選擇捕獲或者不捕獲。但如果拋出檢查異常,在編譯時就必須選擇捕獲或者聲明。
finally語句塊
無論是否有異常,finally塊中的代碼總是會被執行的。 finally語句在執行關閉資源的語句時非常有用。
//第一種形式 //第二種形式 try { try { //執行程序代碼,可能會出現異常 //執行程序代碼,可能會出現異常 }catch(Exception e) { }finally { //捕獲異常並處理 //必執行的代碼 }finally { } //必執行的代碼 }
try-catch-finally的執行流程
try塊中引起異常,異常代碼之后的語句不再執行,若被catch塊捕獲,執行匹配的catch塊,然后執行finally語句。若catch塊不能捕獲異常,則執行finally語句,之后將異常傳遞給這個方法的調用者。
Scanner sc = new Scanner(System.in); int a = 0; //保證局部變量a在各個塊中可用 try { a = sc.nextInt(); if(a < 0) throw new IllegalArgumentException(); System.out.println("執行完try語句。a:" + a); }catch(IllegalArgumentException e){ System.out.println("執行catch語句"); System.out.println("數值小於0,不符合。已設為默認值 1"); a = 1; }finally { System.out.println("執行finally語句。a:" + a); }
//未引發異常 //引發異常並捕獲 5 -5 執行完try語句。a:5 執行catch語句 執行finally語句。a:5 數值小於0,不符合。已設為默認值 1 執行finally語句。a:1
try-finally的執行流程
try塊中引起異常,異常代碼之后的語句不再執行,直接執行finally語句。 try塊沒有引發異常,則執行完try塊就執行finally語句。
try-finally可用在不需要捕獲異常的代碼,可以保證資源在使用后被關閉。例如IO流中執行完相應操作后,關閉相應資源;使用Lock對象保證線程同步,通過finally可以保證鎖會被釋放;數據庫連接代碼時,關閉連接操作等等。
//以Lock加鎖為例,演示try-finally
ReentrantLock lock = new ReentrantLock(); try { //需要加鎖的代碼 }finally { lock.unlock(); //保證鎖一定被釋放 }
- 在前面的代碼中用了System.exit()退出程序。
- finally語句塊中發生了異常。
- 程序所在的線程死亡。
- 關閉CPU。
try、catch、finally、throw和throws使用歸納
- try、catch和finally都不能單獨使用,只能是try-catch、try-finally或者try-catch-finally。
- try語句塊監控代碼,出現異常就停止執行下面的代碼,然后將異常移交給catch語句塊來處理。
- finally語句塊中的代碼一定會被執行,常用於回收資源 。
- throws:聲明一個異常,告知方法調用者。
- throw :拋出一個異常,至於該異常被捕獲還是繼續拋出都與它無關。
- 在恰當的級別處理問題。(在知道該如何處理的情況下了捕獲異常。)
- 解決問題並且重新調用產生異常的方法。
- 進行少許修補,然后繞過異常發生的地方繼續執行。
- 用別的數據進行計算,以代替方法預計會返回的值。
- 把當前運行環境下能做的事盡量做完,然后把相同的異常重拋到更高層。
- 把當前運行環境下能做的事盡量做完,然后把不同的異常拋到更高層。
- 終止程序。
- 進行簡化(如果你的異常模式使問題變得太復雜,那么用起來會非常痛苦)。
- 讓類庫和程序更安全。
自定義異常
通過繼承Exception類來定義一個異常類。Java已經提供了很多異常類,盡量使用這些異常類而不要創建自己的異常類。除非Java的異常類不能很好地描述問題時,才自定義異常來進行准確描述。對於自定義異常,不需要太多功能,類名能准確描述問題是關鍵。
自定義異常如下所示:
//判斷長度是否合法的自定義異常 public class LengthException extends Exception { public LengthException() {} public LengthException(String s) { super(s); } @Override public String getMessage() { return super.getMessage(); } }
