JAVA基礎——異常詳解
轉自:https://www.cnblogs.com/hysum/p/7112011.html
目錄
一、異常簡介
什麼是異常?
異常就是有異於常態,和正常情況不一樣,有錯誤出錯。在java中,阻止當前方法或作用域的情況,稱之為異常。
java中異常的體系是怎麼樣的呢?
1.Java中的所有不正常類都繼承於Throwable類。Throwable主要包括兩個大類,一個是Error類,另一個是Exception類;
2.其中Error類中包括虛擬機器錯誤和執行緒死鎖,一旦Error出現了,程式就徹底的掛了,被稱為程式終結者;
3.Exception類,也就是通常所說的“異常”。主要指編碼、環境、使用者操作輸入出現問題,Exception主要包括兩大類,非檢查異常(RuntimeException)和檢查異常(其他的一些異常)
4.RuntimeException異常主要包括以下四種異常(其實還有很多其他異常,這裡不一一列出):空指標異常、陣列下標越界異常、型別轉換異常、算術異常。RuntimeException異常會由java虛擬機器自動丟擲並自動捕獲(就算我們沒寫異常捕獲語句執行時也會丟擲錯誤!!)
5.檢查異常,引起該異常的原因多種多樣,比如說檔案不存在、或者是連線錯誤等等。跟它的“兄弟”RuntimeException執行異常不同,該異常我們必須手動在程式碼裡新增捕獲語句來處理該異常,這也是我們學習java異常語句中主要處理的異常物件。
二、try-catch-finally語句
(1)try塊:負責捕獲異常,一旦try中發現異常,程式的控制權將被移交給catch塊中的異常處理程式。
【try語句塊不可以獨立存在,必須與 catch 或者 finally 塊同存】
(2)catch塊:如何處理?比如發出警告:提示、檢查配置、網路連線,記錄錯誤等。執行完catch塊之後程式跳出catch塊,繼續執行後面的程式碼。
【編寫catch塊的注意事項:多個catch塊處理的異常類,要按照先catch子類後catch父類的處理方式,因為會【就近處理】異常(由上自下)。】
(3)finally:最終執行的程式碼,用於關閉和釋放資源。
=======================================================================
語法格式如下:
try{
//一些會丟擲的異常
}catch(Exception e){
//第一個catch
//處理該異常的程式碼塊
}catch(Exception e){
//第二個catch,可以有多個catch
//處理該異常的程式碼塊
}finally{
//最終要執行的程式碼
}
當異常出現時,程式將終止執行,交由異常處理程式(丟擲提醒或記錄日誌等),異常程式碼塊外程式碼正常執行。 try會丟擲很多種型別的異常,由多個catch塊捕獲多鍾錯誤。
多重異常處理程式碼塊順序問題:先子類再父類(順序不對編譯器會提醒錯誤),finally語句塊處理最終將要執行的程式碼。
=======================================================================
接下來,我們用例項來鞏固try-catch語句吧~
先看例子:
1 package com.hysum.test;
2
3 public class TryCatchTest {
4 /**
5 * divider:除數
6 * result:結果
7 * try-catch捕獲while迴圈
8 * 每次迴圈,divider減一,result=result+100/divider
9 * 如果:捕獲異常,列印輸出“異常丟擲了”,返回-1
10 * 否則:返回result
11 * @return
12 */
13 public int test1(){
14 int divider=10;
15 int result=100;
16 try{
17 while(divider>-1){
18 divider--;
19 result=result+100/divider;
20 }
21 return result;
22 }catch(Exception e){
23 e.printStackTrace();
24 System.out.println("異常丟擲了!!");
25 return -1;
26 }
27 }
28 public static void main(String[] args) {
29 // TODO Auto-generated method stub
30 TryCatchTest t1=new TryCatchTest();
31 System.out.println("test1方法執行完畢!result的值為:"+t1.test1());
32 }
33
34 }
執行結果:
結果分析:結果中的紅色字丟擲的異常資訊是由e.printStackTrace()來輸出的,它說明了這裡我們丟擲的異常型別是算數異常,後面還跟著原因:by zero(由0造成的算數異常),下面兩行at表明了造成此異常的程式碼具體位置。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
在上面例子中再加上一個test2()方法來測試finally語句的執行狀況:
1 /**
2 * divider:除數
3 * result:結果
4 * try-catch捕獲while迴圈
5 * 每次迴圈,divider減一,result=result+100/divider
6 * 如果:捕獲異常,列印輸出“異常丟擲了”,返回result=999
7 * 否則:返回result
8 * finally:列印輸出“這是finally,哈哈哈!!”同時列印輸出result
9 * @return
10 */
11 public int test2(){
12 int divider=10;
13 int result=100;
14 try{
15 while(divider>-1){
16 divider--;
17 result=result+100/divider;
18 }
19 return result;
20 }catch(Exception e){
21 e.printStackTrace();
22 System.out.println("異常丟擲了!!");
23 return result=999;
24 }finally{
25 System.out.println("這是finally,哈哈哈!!");
26 System.out.println("result的值為:"+result);
27 }
28
29 }
30
31
32
33 public static void main(String[] args) {
34 // TODO Auto-generated method stub
35 TryCatchTest t1=new TryCatchTest();
36 //System.out.println("test1方法執行完畢!result的值為:"+t1.test1());
37 t1.test2();
38 System.out.println("test2方法執行完畢!");
39 }
執行結果:
結果分析:我們可以從結果看出,finally語句塊是在try塊和catch塊語句執行之後最後執行的。finally是在return後面的表示式運算後執行的(此時並沒有返回運算後的值,而是先把要返回的值儲存起來,管finally中的程式碼怎麼樣,返回的值都不會改變,仍然是之前儲存的值),所以函式返回值是在finally執行前確定的;
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
這裡有個有趣的問題,如果把上述中的test2方法中的finally語句塊中加上return,編譯器就會提示警告:finally block does not complete normally
1 public int test2(){
2 int divider=10;
3 int result=100;
4 try{
5 while(divider>-1){
6 divider--;
7 result=result+100/divider;
8 }
9 return result;
10 }catch(Exception e){
11 e.printStackTrace();
12 System.out.println("異常丟擲了!!");
13 return result=999;
14 }finally{
15 System.out.println("這是finally,哈哈哈!!");
16 System.out.println("result的值為:"+result);
17 return result;//編譯器警告
18 }
19
20 }
分析問題: finally塊中的return語句可能會覆蓋try塊、catch塊中的return語句;如果finally塊中包含了return語句,即使前面的catch塊重新丟擲了異常,則呼叫該方法的語句也不會獲得catch塊重新丟擲的異常,而是會得到finally塊的返回值,並且不會捕獲異常。
解決問題:面對上述情況,其實更合理的做法是,既不在try block內部中使用return語句,也不在finally內部使用 return語句,而應該在 finally 語句之後使用return來表示函式的結束和返回。如:
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
總結:
1、不管有木有出現異常或者try和catch中有返回值return,finally塊中程式碼都會執行;
2、finally中最好不要包含return,否則程式會提前退出,返回會覆蓋try或catch中儲存的返回值。
3. e.printStackTrace()可以輸出異常資訊。
4. return值為-1為丟擲異常的習慣寫法。
5. 如果方法中try,catch,finally中沒有返回語句,則會呼叫這三個語句塊之外的return結果。
6. finally 在try中的return之後 在返回主調函式之前執行。
三、throw和throws關鍵字
java中的異常丟擲通常使用throw和throws關鍵字來實現。
throw ----將產生的異常丟擲,是丟擲異常的一個動作。
一般會用於程式出現某種邏輯時程式設計師主動丟擲某種特定型別的異常。如:
語法:throw (異常物件),如:
1 public static void main(String[] args) {
2 String s = "abc";
3 if(s.equals("abc")) {
4 throw new NumberFormatException();
5 } else {
6 System.out.println(s);
7 }
8 //function();
9 }
執行結果:
Exception in thread "main" java.lang.NumberFormatException
at test.ExceptionTest.main(ExceptionTest.java:67)
throws----宣告將要丟擲何種型別的異常(宣告)。
語法格式:
1 public void 方法名(引數列表)
2 throws 異常列表{
3 //呼叫會丟擲異常的方法或者:
4 throw new Exception();
5 }
當某個方法可能會丟擲某種異常時用於throws 宣告可能丟擲的異常,然後交給上層呼叫它的方法程式處理。如:
1 public static void function() throws NumberFormatException{
2 String s = "abc";
3 System.out.println(Double.parseDouble(s));
4 }
5
6 public static void main(String[] args) {
7 try {
8 function();
9 } catch (NumberFormatException e) {
10 System.err.println("非資料型別不能轉換。");
11 //e.printStackTrace();
12 }
13 }
throw與throws的比較
1、throws出現在方法函式頭;而throw出現在函式體。
2、throws表示出現異常的一種可能性,並不一定會發生這些異常;throw則是丟擲了異常,執行throw則一定丟擲了某種異常物件。
3、兩者都是消極處理異常的方式(這裡的消極並不是說這種方式不好),只是丟擲或者可能丟擲異常,但是不會由函式去處理異常,真正的處理異常由函式的上層呼叫處理。
來看個例子:
throws e1,e2,e3只是告訴程式這個方法可能會丟擲這些異常,方法的呼叫者可能要處理這些異常,而這些異常e1,e2,e3可能是該函式體產生的。
throw則是明確了這個地方要丟擲這個異常。如:
1 void doA(int a) throws (Exception1,Exception2,Exception3){
2 try{
3 ......
4
5 }catch(Exception1 e){
6 throw e;
7 }catch(Exception2 e){
8 System.out.println("出錯了!");
9 }
10 if(a!=b)
11 throw new Exception3("自定義異常");
12 }
分析:
1.程式碼塊中可能會產生3個異常,(Exception1,Exception2,Exception3)。
2.如果產生Exception1異常,則捕獲之後再丟擲,由該方法的呼叫者去處理。
3.如果產生Exception2異常,則該方法自己處理了(即System.out.println("出錯了!");)。所以該方法就不會再向外丟擲Exception2異常了,void doA() throws Exception1,Exception3 裡面的Exception2也就不用寫了。因為已經用try-catch語句捕獲並處理了。
4.Exception3異常是該方法的某段邏輯出錯,程式設計師自己做了處理,在該段邏輯錯誤的情況下丟擲異常Exception3,則該方法的呼叫者也要處理此異常。這裡用到了自定義異常,該異常下面會由解釋。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
使用throw和throws關鍵字需要注意以下幾點:
1.throws的異常列表可以是丟擲一條異常,也可以是丟擲多條異常,每個型別的異常中間用逗號隔開
2.方法體中呼叫會丟擲異常的方法或者是先丟擲一個異常:用throw new Exception() throw寫在方法體裡,表示“丟擲異常”這個動作。
3.如果某個方法呼叫了丟擲異常的方法,那麼必須新增try catch語句去嘗試捕獲這種異常, 或者新增宣告,將異常丟擲給更上一層的呼叫者進行處理
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
自定義異常
為什麼要使用自定義異常,有什麼好處?
1.我們在工作的時候,專案是分模組或者分功能開發的 ,基本不會你一個人開發一整個專案,使用自定義異常類就統一了對外異常展示的方式。
2.有時候我們遇到某些校驗或者問題時,需要直接結束掉當前的請求,這時便可以通過丟擲自定義異常來結束,如果你專案中使用了SpringMVC比較新的版本的話有控制器增強,可以通過@ControllerAdvice註解寫一個控制器增強類來攔截自定義的異常並響應給前端相應的資訊。
3.自定義異常可以在我們專案中某些特殊的業務邏輯時丟擲異常,比如"中性".equals(sex),性別等於中性時我們要丟擲異常,而Java是不會有這種異常的。系統中有些錯誤是符合Java語法的,但不符合我們專案的業務邏輯。
4.使用自定義異常繼承相關的異常來丟擲處理後的異常資訊可以隱藏底層的異常,這樣更安全,異常資訊也更加的直觀。自定義異常可以丟擲我們自己想要丟擲的資訊,可以通過丟擲的資訊區分異常發生的位置,根據異常名我們就可以知道哪裡有異常,根據異常提示資訊進行程式修改。比如空指標異常NullPointException,我們可以丟擲資訊為“xxx為空”定位異常位置,而不用輸出堆疊資訊。
說完了為什麼要使用自定義異常,有什麼好處,我們再來看看自定義異常的毛病:
毋庸置疑,我們不可能期待JVM(Java虛擬機器)自動丟擲一個自定義異常,也不能夠期待JVM會自動處理一個自定義異常。發現異常、丟擲異常以及處理異常的工作必須靠程式設計人員在程式碼中利用異常處理機制自己完成。這樣就相應的增加了一些開發成本和工作量,所以專案沒必要的話,也不一定非得要用上自定義異常,要能夠自己去權衡。
最後,我們來看看怎麼使用自定義異常:
在 Java 中你可以自定義異常。編寫自己的異常類時需要記住下面的幾點。
- 所有異常都必須是 Throwable 的子類。
- 如果希望寫一個檢查性異常類,則需要繼承 Exception 類。
- 如果你想寫一個執行時異常類,那麼需要繼承 RuntimeException 類。
可以像下面這樣定義自己的異常類:
class MyException extends Exception{ }
我們來看一個例項:
1 package com.hysum.test;
2
3 public class MyException extends Exception {
4 /**
5 * 錯誤編碼
6 */
7 private String errorCode;
8
9
10 public MyException(){}
11
12 /**
13 * 構造一個基本異常.
14 *
15 * @param message
16 * 資訊描述
17 */
18 public MyException(String message)
19 {
20 super(message);
21 }
22
23
24
25 public String getErrorCode() {
26 return errorCode;
27 }
28
29 public void setErrorCode(String errorCode) {
30 this.errorCode = errorCode;
31 }
32
33
34 }
使用自定義異常丟擲異常資訊:
1 package com.hysum.test;
2
3 public class Main {
4
5 public static void main(String[] args) {
6 // TODO Auto-generated method stub
7 String[] sexs = {"男性","女性","中性"};
8 for(int i = 0; i < sexs.length; i++){
9 if("中性".equals(sexs[i])){
10 try {
11 throw new MyException("不存在中性的人!");
12 } catch (MyException e) {
13 // TODO Auto-generated catch block
14 e.printStackTrace();
15 }
16 }else{
17 System.out.println(sexs[i]);
18 }
19 }
20 }
21
22 }
執行結果:
就是這麼簡單,可以根據實際業務需求去丟擲相應的自定義異常。
四、java中的異常鏈
異常需要封裝,但是僅僅封裝還是不夠的,還需要傳遞異常。
異常鏈是一種面向物件程式設計技術,指將捕獲的異常包裝進一個新的異常中並重新丟擲的異常處理方式。原異常被儲存為新異常的一個屬性(比如cause)。這樣做的意義是一個方法應該丟擲定義在相同的抽象層次上的異常,但不會丟棄更低層次的資訊。
我可以這樣理解異常鏈:
把捕獲的異常包裝成新的異常,在新異常裡新增原始的異常,並將新異常丟擲,它們就像是鏈式反應一樣,一個導致(cause)另一個。這樣在最後的頂層丟擲的異常資訊就包括了最底層的異常資訊。
》場景
比如我們的JEE專案一般都又三層:持久層、邏輯層、展現層,持久層負責與資料庫互動,邏輯層負責業務邏輯的實現,展現層負責UI資料的處理。
有這樣一個模組:使用者第一次訪問的時候,需要持久層從user.xml中讀取資料,如果該檔案不存在則提示使用者建立之,那問題就來了:如果我們直接把持久層的異常FileNotFoundException拋棄掉,邏輯層根本無從得知發生任何事情,也就不能為展現層提供一個友好的處理結果,最終倒黴的就是展現層:沒有辦法提供異常資訊,只能告訴使用者“出錯了,我也不知道出了什麼錯了”—毫無友好性而言。
正確的做法是先封裝,然後傳遞,過程如下:
1.把FileNotFoundException封裝為MyException。
2.丟擲到邏輯層,邏輯層根據異常程式碼(或者自定義的異常型別)確定後續處理邏輯,然後丟擲到展現層。
3.展現層自行確定展現什麼,如果管理員則可以展現低層級的異常,如果是普通使用者則展示封裝後的異常。
》示例
1 package com.hysum.test;
2
3 public class Main {
4 public void test1() throws RuntimeException{
5 String[] sexs = {"男性","女性","中性"};
6 for(int i = 0; i < sexs.length; i++){
7 if("中性".equals(sexs[i])){
8 try {
9 throw new MyException("不存在中性的人!");
10 } catch (MyException e) {
11 // TODO Auto-generated catch block
12 e.printStackTrace();
13 RuntimeException rte=new RuntimeException(e);//包裝成RuntimeException異常
14 //rte.initCause(e);
15 throw rte;//丟擲包裝後的新的異常
16 }
17 }else{
18 System.out.println(sexs[i]);
19 }
20 }
21 }
22 public static void main(String[] args) {
23 // TODO Auto-generated method stub
24 Main m =new Main();
25
26 try{
27 m.test1();
28 }catch (Exception e){
29 e.printStackTrace();
30 e.getCause();//獲得原始異常
31 }
32
33 }
34
35 }
執行結果:
結果分析:我們可以看到控制檯先是輸出了原始異常,這是由e.getCause()輸出的;然後輸出了e.printStackTrace(),在這裡可以看到Caused by:原始異常和e.getCause()輸出的一致。這樣就是形成一個異常鏈。initCause()的作用是包裝原始的異常,當想要知道底層發生了什麼異常的時候呼叫getCause()就能獲得原始異常。
》建議
異常需要封裝和傳遞,我們在進行系統開發的時候,不要“吞噬”異常,也不要“赤裸裸”的丟擲異常,封裝後在丟擲,或者通過異常鏈傳遞,可以達到系統更健壯、友好的目的。
五、結束語
java的異常處理的知識點雜而且理解起來也有點困難,我在這裡給大家總結了以下幾點使用java異常處理的時候,良好的編碼習慣:
1、處理執行時異常時,採用邏輯去合理規避同時輔助try-catch處理
2、在多重catch塊後面,可以加一個catch(Exception)來處理可能會被遺漏的異常
3、對於不確定的程式碼,也可以加上try-catch,處理潛在的異常
4、儘量去處理異常,切記只是簡單的呼叫printStackTrace()去列印
5、具體如何處理異常,要根據不同的業務需求和異常型別去決定
6、儘量新增finally語句塊去釋放佔用的資源