本文將詳解java中的異常和異常處理機制
異常簡介
什么是異常?
程序運行時,發生的不被期望的事件,它阻止了程序按照程序員的預期正常執行,這就是異常。
Java異常的分類和類結構圖
1.Java中的所有不正常類都繼承於Throwable類。Throwable主要包括兩個大類,一個是Error類,另一個是Exception類;
錯誤:Error類以及他的子類的實例,代表了JVM本身的錯誤。包括虛擬機錯誤和線程死鎖,一旦Error出現了,程序就徹底的掛了,被稱為程序終結者;例如,JVM 內存溢出。一般地,程序不會從錯誤中恢復。
異常:Exception以及他的子類,代表程序運行時發生的各種不期望發生的事件。可以被Java異常處理機制使用,是異常處理的核心。Exception主要包括兩大類,非檢查異常(RuntimeException)和檢查異常(其他的一些異常)
非檢查異常(unckecked exception):Error 和 RuntimeException 以及他們的子類。javac在編譯時,不會提示和發現這樣的異常,不要求在程序處理這些異常。所以如果願意,我們可以編寫代碼處理(使用try…catch…finally)這樣的異常,也可以不處理。對於這些異常,我們應該修正代碼,而不是去通過異常處理器處理 。這樣的異常發生的原因多半是代碼寫的有問題。如除0錯誤ArithmeticException,錯誤的強制類型轉換錯誤ClassCastException,數組索引越界ArrayIndexOutOfBoundsException,使用了空對象NullPointerException等等。
檢查異常(checked exception):除了Error 和 RuntimeException的其它異常。javac強制要求程序員為這樣的異常做預備處理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch語句捕獲它並處理,要么用throws子句聲明拋出它,否則編譯不會通過。這樣的異常一般是由程序的運行環境導致的。因為程序可能被運行在各種未知的環境下,而程序員無法干預用戶如何使用他編寫的程序,於是程序員就應該為這樣的異常時刻准備着。如SQLException , IOException,ClassNotFoundException 等。
捕獲異常
基礎語法
(1)try塊:負責捕獲異常,一旦try中發現異常,程序的控制權將被移交給catch塊中的異常處理程序。
【try語句塊不可以獨立存在,必須與 catch 或者 finally 塊同存】
(2)catch塊:如何處理?比如發出警告:提示、檢查配置、網絡連接,記錄錯誤等。執行完catch塊之后程序跳出catch塊,繼續執行后面的代碼。
【編寫catch塊的注意事項:多個catch塊處理的異常類,要按照先catch子類后catch父類的處理方式,因為會【就近處理】異常(由上自下)。】
(3)finally:最終執行的代碼,用於關閉和釋放資源。
語法格式如下:
try { //一些會拋出的異常 }catch(ExceptionName e1){ //處理該異常的代碼塊 }finally{ //最終要執行的代碼 }
java中,異常處理的任務就是將執行控制流從異常發生的地方轉移到能夠處理這種異常的地方去。也就是說:當一個函數的某條語句發生異常時,這條語句的后面的語句不會再執行,它失去了焦點。執行流跳轉到最近的匹配的異常處理catch代碼塊去執行,異常被處理完后,執行流會接着在“處理了這個異常的catch代碼塊”后面接着執行。
public static void main(String[] args){ try { foo(); }catch(ArithmeticException ae) { System.out.println("處理異常"); } } public static void foo(){ int a = 5/0; //異常拋出點 System.out.println("為什么還不給我漲工資!!!"); //////////////////////不會執行 }
多重捕獲塊
一個 try 代碼塊后面跟隨多個 catch 代碼塊的情況就叫多重捕獲。
多重捕獲塊的語法如下所示:
try { file = new FileInputStream(fileName); x = (byte) file.read(); } catch(FileNotFoundException f) { // Not valid! f.printStackTrace(); return -1; } catch(IOException i) { i.printStackTrace(); return -1; }
如果保護代碼中發生異常,異常被拋給第一個 catch 塊。
如果拋出異常的數據類型與 ExceptionType1 匹配,它在這里就會被捕獲。
如果不匹配,它會被傳遞給第二個 catch 塊。
如此,直到異常被捕獲或者通過所有的 catch 塊。
多重異常處理代碼塊順序問題:先子類再父類(順序不對編譯器會提醒錯誤),finally語句塊處理最終將要執行的代碼。
throw和throws關鍵字
java中的異常拋出通常使用throw和throws關鍵字來實現。
throw 異常拋出語句
throw ----將產生的異常拋出,是拋出異常的一個動作。
如果不使用try catch語句去嘗試捕獲這種異常, 或者添加聲明,將異常拋出給更上一層的調用者進行處理,則程序將會在這里停止,並不會執行剩下的代碼。
一般會用於程序出現某種邏輯時程序員主動拋出某種特定類型的異常。如:
語法:throw (異常對象),如:
public static void main(String[] args) { String s = "abc"; if(s.equals("abc")) { throw new NumberFormatException(); } else { System.out.println(s); } //function(); }
運行結果:
Exception in thread "main" java.lang.NumberFormatException
at test.ExceptionTest.main(ExceptionTest.java:67)
throws 函數聲明
throws----聲明將要拋出何種類型的異常(聲明)。
當某個方法可能會拋出某種異常時用於throws 聲明可能拋出的異常,然后交給上層調用它的方法程序處理。如:
public static void function() throws NumberFormatException{ String s = "abc"; System.out.println(Double.parseDouble(s)); } public static void main(String[] args) { try { function(); } catch (NumberFormatException e) { System.err.println("非數據類型不能轉換。"); //e.printStackTrace(); } }
throw與throws的比較
1、throws出現在方法函數頭;而throw出現在函數體。
2、throws表示出現異常的一種可能性,並不一定會發生這些異常;throw則是拋出了異常,執行throw則一定拋出了某種異常對象。
3、兩者都是消極處理異常的方式(這里的消極並不是說這種方式不好),只是拋出或者可能拋出異常,但是不會由函數去處理異常,真正的處理異常由函數的上層調用處理。
來看個例子:
throws e1,e2,e3只是告訴程序這個方法可能會拋出這些異常,方法的調用者可能要處理這些異常,而這些異常e1,e2,e3可能是該函數體產生的。
throw則是明確了這個地方要拋出這個異常。如:
void doA(int a) throws (Exception1,Exception2,Exception3){ try{ ...... }catch(Exception1 e){ throw e; }catch(Exception2 e){ System.out.println("出錯了!"); } if(a!=b) throw new Exception3("自定義異常"); }
分析:
1.代碼塊中可能會產生3個異常,(Exception1,Exception2,Exception3)。
2.如果產生Exception1異常,則捕獲之后再拋出,由該方法的調用者去處理。
3.如果產生Exception2異常,則該方法自己處理了(即System.out.println("出錯了!");)。所以該方法就不會再向外拋出Exception2異常了,void doA() throws Exception1,Exception3 里面的Exception2也就不用寫了。因為已經用try-catch語句捕獲並處理了。
4.Exception3異常是該方法的某段邏輯出錯,程序員自己做了處理,在該段邏輯錯誤的情況下拋出異常Exception3,則該方法的調用者也要處理此異常。這里用到了自定義異常,該異常下面會由解釋。
注意:如果某個方法調用了拋出異常的方法,那么必須添加try catch語句去嘗試捕獲這種異常, 或者添加聲明,將異常拋出給更上一層的調用者進行處理
自定義異常
為什么要使用自定義異常,有什么好處?
1.我們在工作的時候,項目是分模塊或者分功能開發的 ,基本不會你一個人開發一整個項目,使用自定義異常類就統一了對外異常展示的方式。
2.有時候我們遇到某些校驗或者問題時,需要直接結束掉當前的請求,這時便可以通過拋出自定義異常來結束,如果你項目中使用了SpringMVC比較新的版本的話有控制器增強,可以通過@ControllerAdvice注解寫一個控制器增強類來攔截自定義的異常並響應給前端相應的信息。
3.自定義異常可以在我們項目中某些特殊的業務邏輯時拋出異常,比如"中性".equals(sex),性別等於中性時我們要拋出異常,而Java是不會有這種異常的。系統中有些錯誤是符合Java語法的,但不符合我們項目的業務邏輯。
怎么使用自定義異常?
在 Java 中你可以自定義異常。編寫自己的異常類時需要記住下面的幾點。
- 所有異常都必須是 Throwable 的子類。
- 如果希望寫一個檢查性異常類,則需要繼承 Exception 類。
- 如果你想寫一個運行時異常類,那么需要繼承 RuntimeException 類。
可以像下面這樣定義自己的異常類:
class MyException extends Exception{ }
我們來看一個實例:
package com.hysum.test; public class MyException extends Exception { /** * 錯誤編碼 */ private String errorCode; public MyException(){} /** * 構造一個基本異常. * * @param message * 信息描述 */ public MyException(String message){ super(message); } public String getErrorCode() { return errorCode; } public void setErrorCode(String errorCode) { this.errorCode = errorCode; } }
使用自定義異常拋出異常信息:
package com.hysum.test; public class Main { public static void main(String[] args) { // TODO Auto-generated method stub String[] sexs = {"男性","女性","中性"}; for(int i = 0; i < sexs.length; i++){ if("中性".equals(sexs[i])){ try { throw new MyException("不存在中性的人!"); } catch (MyException e) { // TODO Auto-generated catch block e.printStackTrace(); } }else{ System.out.println(sexs[i]); } } } }
運行結果:
就是這么簡單,可以根據實際業務需求去拋出相應的自定義異常。
finally塊和return
首先一個不容易理解的事實:在 try塊中即便有return,break,continue等改變執行流的語句,finally也會執行。
public static void main(String[] args) { int re = bar(); System.out.println(re); } private static int bar() { try{ return 5; } finally{ System.out.println("finally"); } } /*輸出: finally
5 */
也就是說:try…catch…finally中的return 只要能執行,就都執行了,他們共同向同一個內存地址(假設地址是0×80)寫入返回值,后執行的將覆蓋先執行的數據,而真正被調用者取的返回值就是最后一次寫入的。那么,按照這個思想,下面的這個例子也就不難理解了。
finally中的return 會覆蓋 try 或者catch中的返回值。
public static void main(String[] args){ int result; result = foo(); System.out.println(result); /////////2 result = bar(); System.out.println(result); /////////2 } @SuppressWarnings("finally") public static int foo(){ trz{ int a = 5 / 0; } catch (Exception e){ return 1; } finally{ return 2; } } @SuppressWarnings("finally") public static int bar(){ try { return 1; }finally { return 2; } }
finally中的return會抑制(消滅)前面try或者catch塊中的異常
class TestException{ public static void main(String[] args){ int result; try{ result = foo(); System.out.println(result); //輸出100 } catch (Exception e){ System.out.println(e.getMessage()); //沒有捕獲到異常 } try{ result = bar(); System.out.println(result); //輸出100 } catch (Exception e){ System.out.println(e.getMessage()); //沒有捕獲到異常 } } //catch中的異常被抑制 @SuppressWarnings("finally") public static int foo() throws Exception{ try { int a = 5/0; return 1; }catch(ArithmeticException amExp) { throw new Exception("我將被忽略,因為下面的finally中使用了return"); }finally { return 100; } } //try中的異常被抑制 @SuppressWarnings("finally") public static int bar() throws Exception{ try { int a = 5/0; return 1; }finally { return 100; } } }
finally中的異常會覆蓋(消滅)前面try或者catch中的異常
class TestException{ public static void main(String[] args){ int result; try{ result = foo(); } catch (Exception e){ System.out.println(e.getMessage()); //輸出:我是finaly中的Exception } try{ result = bar(); } catch (Exception e){ System.out.println(e.getMessage()); //輸出:我是finaly中的Exception } } //catch中的異常被抑制 @SuppressWarnings("finally") public static int foo() throws Exception{ try { int a = 5/0; return 1; }catch(ArithmeticException amExp) { throw new Exception("我將被忽略,因為下面的finally中拋出了新的異常"); }finally { throw new Exception("我是finaly中的Exception"); } } //try中的異常被抑制 @SuppressWarnings("finally") public static int bar() throws Exception{ try { int a = 5/0; return 1; }finally { throw new Exception("我是finaly中的Exception"); } } }
上面的3個例子都異於常人的編碼思維,因此我建議:
- 不要在fianlly中使用return。
- 不要在finally中拋出異常。
- 減輕finally的任務,不要在finally中做一些其它的事情,finally塊僅僅用來釋放資源是最合適的。
- 將盡量將所有的return寫在函數的最后面,而不是try … catch … finally中。
Spring異常處理 ExceptionHandler的使用
通常一個web程序在運行過程中,由於用戶的操作不當,或者程序的bug,有大量需要處理的異常。其中有些異常是需要暴露給用戶的,比如登陸超時,權限不足等等。可以通過彈出提示信息的方式告訴用戶出了什么錯誤。
而這就表示在程序中需要一個機制,去處理這些異常,將程序的異常轉換為用戶可讀的異常。而且最重要的,是要將這個機制統一,提供統一的異常處理。
使用加強Controller做全局異常處理
@ExceptionHandler注解。
當一個Controller中有方法加了@ExceptionHandler之后,這個Controller其他方法中沒有捕獲的異常就會以參數的形式傳入加了@ExceptionHandler注解的那個方法中。
首先需要為自己的系統設計一個自定義的異常類,通過它來傳遞狀態碼。
/** Created by ChenHao. * 自定義異常 */ public class SystemException extends RuntimeException{ private String code;//狀態碼 public SystemException(String message, String code) { super(message); this.code = code; } public String getCode() { return code; } }
所謂加強Controller就是@ControllerAdvice注解,有這個注解的類中的方法的某些注解會應用到所有的Controller里,其中就包括@ExceptionHandler注解。
於是可以寫一個全局的異常處理類:
/** * Created by ChenHao on 2019/02/26. * 全局異常處理,捕獲所有Controller中拋出的異常。 */ @ControllerAdvice public class GlobalExceptionHandler { //處理自定義的異常 @ExceptionHandler(SystemException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Object customHandler(SystemException e){ e.printStackTrace(); return WebResult.buildResult().status(e.getCode()).msg(e.getMessage()); } //其他未處理的異常 @ExceptionHandler(Exception.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Object exceptionHandler(Exception e){ e.printStackTrace(); return WebResult.buildResult().status(Config.FAIL).msg("系統錯誤"); } }
這個類中只處理了兩個異常,但是已經滿足了大部分需要,如果還有需要特殊處理的地方,可以再加上處理的方法就行了。
如此,我們現在的Controller中的方法就可以很簡潔了,比如處理登陸的邏輯就可以這樣簡單的寫:
/** * Created by ChenHao on 2016/12/28. * 賬號 */ @RestController @RequestMapping("passport") public class PassportController { PassportService passportService; @RequestMapping("login") public Object doLogin(HttpSession session, String username, String password){ User user = passportService.doLogin(username, password); session.setAttribute("user", user); return WebResult.buildResult().redirectUrl("/student/index"); } }
而在passprotService的doLogin方法中,可能會拋出用戶名或密碼錯誤等異常,然后就會交由GlobalExceptionHandler去處理,直接返回異常信息給前端,然后前端也不需要關心是否返回了異常,因為這些都已經定義好了。
一個異常在其中流轉的過程為:
比如doLogin方法拋出了自定義異常,其code為:FAIL,message為:用戶名或密碼錯誤,由於在controller的方法中沒有捕獲這個異常,所以會將異常拋給GlobalExceptionHandler,然后GlobalExceptionHandler通過WebResult將狀態碼和提示信息返回給前端,前端通過默認的處理函數,彈框提示用戶“用戶名或密碼錯誤”。而對於這樣的一次交互,我們根本不用編寫異常處理部分的邏輯。