天天看點

Java的異常處理

一、介紹

  在認為編寫的程式中不可能沒有一點錯誤,當程式運作時,發生了編寫者不希望的事件,阻止了程式的正常執行,這就是異常。當異常出現時,我們處理的方式就是本文要介紹的主要内容。

  Java的異常處理機制:

在Java中異常被當作對象來處理,其根類為java.lang.Throwable,Throwable又派生出Error類和Exception類。

Java的異常處理

總體上我們根據Javac對異常的處理要求,将異常類分為2類。

非檢查異常(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 等。

需要明确的是:檢查和非檢查是對于javac來說的,這樣就很好了解和區分了。

二、異常的基本處理方式

  先看一個處理異常的例子:

1 public class yichang {
 2     public static void main(String[] args) {
 3         chufa x = new chufa();
 4         x.chufa1(1,0);
 5     }
 6 }
 7 class chufa{
 8 
 9     public int chufa1(int a,int b){
10         int c = 0;
11         try {
12             c = a/b;
13         } catch (Exception e) {
14             e.printStackTrace();
15         }
16         return c;
17     }
18 }      
java.lang.ArithmeticException: / by zero
    at chufa.chufa1(yichang.java:14)
    at yichang.main(yichang.java:6)      

  這裡是一個十分基礎的異常處理,在try中的語句發生了異常無法正常往下執行的時候,就會執行catch,而catch這裡有一個Exception e,這是一個異常對象,而在catch裡面執行了這個e對象printStackTrace(),這個方法的作用是将異常抛出,讓調用這個方法的地方去處理這個異常。

  從輸出可以看出,在try語句中出現異常的時候,catch抛出異常,而抛出這個異常就到了main函數裡(mian函數調用了chufa),是以在輸出的第三行也顯示了mian函數出現異常,這可以了解為異常從出現的地方找到了最近調用它的函數,将異常抛給它,類似于異常的冒泡,一切設計到異常的地方都會被顯示。

那如果這裡我們不用異常捕獲,編譯器并不知道這個地方要發生異常,也不會強制我們添加異常捕獲,因為這個異常是非檢查異常

再看看檢查異常下面例子:

1  public void testException()
 2         {
 3         //FileInputStream的構造函數會抛出FileNotFoundException
 4         FileInputStream fileIn=new FileInputStream("E:\\a.txt");
 5 
 6         int word;
 7         //read方法會抛出IOException
 8         while((word=fileIn.read())!=-1)
 9         {
10         System.out.print((char)word);
11         }
12         //close方法會抛出IOException
13         fileIn.close();
14         }      

這段代碼是不能通過編譯的,因為裡面可能會出現檢查異常,編譯器會強制編寫者對異常進行處理

在Java中除了可以用try catch來對異常進行捕獲和處理之外,還有其他兩種方法,共有三種:

1、上文提到try catch

2、可以在方法體外用throws進行抛出聲明,這個抛出會告知調用這個方法的對象,這個方法可能會出現的異常,這裡也有兩種情況:

  • 如果抛出的是非處理異常,調用這個方法的對象可以選擇性的進行異常處理,也就是編譯器不會因為不處理這個異常而報錯
  • 如果抛出的是處理異常,那調用這個方法的對象就必須顯式的處理這個異常,将其抛出到更高層,或者進行其他操作

3、在代碼塊中用throw手動抛出一個異常對象,此時也會有兩種情況:

  • 如果抛出的是處理異常,那調用這個方法的對象就必須顯式的處理這個異常,将其抛出到更高層,或者進行其他操作(與上面一樣)

(如果最終将異常抛給main方法,則相當于交給jvm自動處理,此時jvm會簡單地列印異常資訊)

三、深入認識幾種異常處理的方式

1、Try,catch,finally

  這裡又多出一個fianlly關鍵字,try關鍵字可以配合剩餘兩個關鍵字使用,三個關鍵字有幾種組合方式:

(1)try{代碼塊}                           
     catch{代碼塊}  

(2)try{代碼塊}
    finally{代碼塊}

(3)try{代碼塊}
    catch{代碼塊}
      finally{代碼塊}         

   這裡try是用來捕獲異常,catch是當出現異常時執行的操作,finally不管try裡是否出現異常都會執行,catch塊可以有多個,try和finally隻能有一個,同時finally也可以不添加。

  當有多個catch塊的時候,是按照catch塊的先後順序進行比對,一旦異常被一個catch塊比對,則不會與後面的catch塊進比對——根據這個我們得出,當一個catch塊能捕捉異常的範圍要大于其他catch塊,大範圍的應該放在後面進行比對,如果把大範圍的放在前面,那範圍小的在後面永遠不可能被比對

那再看如下代碼:

1 class test{
 2     public static void main(String[] args) {
 3         TestException t = new TestException();
 4         String x = t.chufa(1,0);
 5         System.out.println(x);
 6     }
 7 }
 8 class TestException{
 9     public String chufa(int a,int b){
10         int p = 0;
11         try {
12             p = a/b;
13         } catch (Exception e) {
14             return "This is catch";
15         }finally {
16             return "This is finally";
17         }
18     }
19 }      

可以考慮一下這一段代碼的輸出:

This is finally      

這三個關鍵字原本的執行順序應該是:

try内代碼塊若沒有出現異常,不會執行catch,直接執行finally

try内代碼塊若出現異常,會先執行catch進行處理異常的操作,然後再執行finally

  我們可以看上面的例子,try裡面出現了異常,這裡肯定先執行了catch,但catch裡的語句直接傳回了,按理應該不會執行finally,但最終結果卻是傳回的是finally的傳回值。

  從這裡可以看出,無論前面是否有return語句,finally塊的語句都會執行。

我們稍微修改一下上面的例子

1  class x{
 2     public static void main(String[] args) {
 3         TestException t = new TestException();
 4         String x = t.chufa(1,0);
 5         System.out.println(x);
 6     }
 7 }
 8 class TestException{
 9     public String chufa(int a,int b){
10         int p = 0;
11         try {
12             p = a/b;
13         } catch (Exception e) {
14             System.out.println("到catch");//試驗是否執行catch
15             return "This is catch";
16         }finally {
17             return "This is finally";
18         }
19     }
20 }      
到catch
This is finally      

  這裡我們徹底明白了,catch是先于finally執行,但catch裡要傳回了,編譯器就直接先執行了finally的代碼塊,讓程式直接傳回,如果finally裡面不是傳回值,則程式還是會在catch裡進行傳回

  是以finally執行的語句可能會覆寫我們到傳回值,是以千萬不要在finally寫任何傳回語句,finally主要的作用就是進行資源的釋放。

2、throws和thow關鍵字

  throws用在方法聲明的地方,表明這個方法可能會抛出某種異常,然後将異常交給調用其到程式來處理,throws後面可以跟着多個異常。

  throw是一定會抛出異常,這是當程式執行到某處的時候編寫者主動去抛出特定異常,隻能用在方法體中,而throw一個很重要的作用就是可以進行異常的轉換,可以抛出想抛出的資訊。

3、繼承關系中的異常

本小節讨論子類重寫父類方法的時候,如何确定異常抛出聲明的類型。下面是三點原則:

  1)父類的方法沒有聲明異常,子類在重寫該方法的時候不能聲明異常;

  2)如果父類的方法聲明一個異常exception1,則子類在重寫該方法的時候聲明的異常不能是exception1的父類;

  3)如果父類的方法聲明的異常類型隻有非運作時異常(運作時異常),則子類在重寫該方法的時候聲明的異常也隻能有非運作時異常(運作時異常),不能含有運作時異常(非運作時異常)。

4、異常進行中的建議

   以下是根據前人總結的一些異常處理的建議:

1.隻在必要使用異常的地方才使用異常,不要用異常去控制程式的流程

  謹慎地使用異常,異常捕獲的代價非常高昂,異常使用過多會嚴重影響程式的性能。如果在程式中能夠用if語句和Boolean變量來進行邏輯判斷,那麼盡量減少異常的使用,進而避免不必要的異常捕獲和處理。比如下面這段經典的程式:

1 public void useExceptionsForFlowControl() {  
 2   try {  
 3   while (true) {  
 4     increaseCount();  
 5     }  
 6   } catch (MaximumCountReachedException ex) {  
 7   }  
 8   //Continue execution  
 9 }  
10     
11 public void increaseCount() throws MaximumCountReachedException {  
12   if (count >= 5000)  
13     throw new MaximumCountReachedException();  
14 }      

  上邊的useExceptionsForFlowControl()用一個無限循環來增加count直到抛出異常,這種做法并沒有說讓代碼不易讀,而是使得程式執行效率降低

2.切忌使用空catch塊

  在捕獲了異常之後什麼都不做,相當于忽略了這個異常。千萬不要使用空的catch塊,空的catch塊意味着你在程式中隐藏了錯誤和異常,并且很可能導緻程式出現不可控的執行結果。如果你非常肯定捕獲到的異常不會以任何方式對程式造成影響,最好用Log日志将該異常進行記錄,以便日後友善更新和維護。

3.檢查異常和非檢查異常的選擇

一旦你決定抛出異常,你就要決定抛出什麼異常。這裡面的主要問題就是抛出檢查異常還是非檢查異常。

  檢查異常導緻了太多的try…catch代碼,可能有很多檢查異常對開發人員來說是無法合理地進行處理的,比如SQLException,而開發人員卻不得不去進行try…catch,這樣就會導緻經常出現這樣一種情況:邏輯代碼隻有很少的幾行,而進行異常捕獲和處理的代碼卻有很多行。這樣不僅導緻邏輯代碼閱讀起來晦澀難懂,而且降低了程式的性能。

  我個人建議盡量避免檢查異常的使用,如果确實該異常情況的出現很普遍,需要提醒調用者注意處理的話,就使用檢查異常;否則使用非檢查異常。

  是以,在一般情況下,我覺得盡量将檢查異常轉變為非檢查異常交給上層處理。

5.不要将提供給使用者看的資訊放在異常資訊裡

1 public class Main {
 2     public static void main(String[] args) {
 3         try {
 4             String user = null;
 5             String pwd = null;
 6             login(user,pwd);
 7         } catch (Exception e) {
 8             System.out.println(e.getMessage());
 9         }
10          
11     }
12      
13     public static void login(String user,String pwd) {
14         if(user==null||pwd==null)
15             throw new NullPointerException("使用者名或者密碼為空");
16         //...
17     }
18 }      

  展示給使用者錯誤提示資訊最好不要跟程式混淆一起,比較好的方式是将所有錯誤提示資訊放在一個配置檔案中統一管理。

6.避免多次在日志資訊中記錄同一個異常

  隻在異常最開始發生的地方進行日志資訊記錄。很多情況下異常都是層層向上跑出的,如果在每次向上抛出的時候,都Log到日志系統中,則會導緻無從查找異常發生的根源。

7. 異常處理盡量放在高層進行

  盡量将異常統一抛給上層調用者,由上層調用者統一之時如何進行處理。如果在每個出現異常的地方都直接進行處理,會導緻程式異常處理流程混亂,不利于後期維護和異常錯誤排查。由上層統一進行處理會使得整個程式的流程清晰易懂。

8. 在finally中釋放資源

  如果有使用檔案讀取、網絡操作以及資料庫操作等,記得在finally中釋放資源。這樣不僅會使得程式占用更少的資源,也會避免不必要的由于資源未釋放而發生的異常情況。 

 參考資料:

https://www.cnblogs.com/dolphin0520/p/3769804.html

https://www.cnblogs.com/lulipro/p/7504267.html

11