天天看點

JAVA異常與異常處理詳解

轉載——JAVA異常與異常處理詳解

轉自https://www.cnblogs.com/knightsu/p/7114914.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

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

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

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語句塊去釋放占用的資源