Java異常捕捉陷阱(記憶體洩漏,finally塊,catch塊,繼承得到的異常)

執筆記憶的空白發表於2016-03-28

 

1.    異常捕捉的陷阱

異常處理機制是java語言的特色之一,尤其是java語言的Checked異常,更是體現了java語言的嚴謹性:沒有完善錯誤處理的程式碼根本不會被執行。對於Checked異常,java程式要麼宣告丟擲,要麼使用try……catch進行捕獲。

1.1  正確關閉資源的方式

在實際開發中,經常需要在程式中開啟一些物理資源,如資料庫連線,網路連線,磁碟檔案等,開啟這些物理資源之後必須顯示關閉,否則將會導致資源洩漏。因為垃圾回收機制屬於java記憶體管理的一部分,它只是負責會受堆記憶體中分配出來的記憶體,至於程式中開啟的物理資源,垃圾回收機制是無能為力的。

為了正常關閉程式中開啟的物理資源,應該使用finally塊來保證回收。比如下面三種關閉資源的方式,哪種更好些?

  1. public static void main(String args[]) throws Exception {  
  2.                    Student student_new = new Student("liyafang");  
  3.                    Student student_recover = null;  
  4.                    ObjectOutputStream oos = null;  
  5.                    ObjectInputStream ois = null;  
  6.                    try {  
  7.                             oos = new ObjectOutputStream(new FileOutputStream("liyafang.txt"));  
  8.                             ois = new ObjectInputStream(new FileInputStream("liyafang.txt"));  
  9.                             oos.writeObject(student_new);  
  10.                             oos.flush();  
  11.                             student_recover = (Student) ois.readObject();  
  12.                             System.out.println(student_recover.equals(student_new));  
  13.                             System.out.println(student_recover == student_new);  
  14.                    } finally {  
  15. //1.第一種關閉資源的方式(不夠安全):程式剛開始指定oos = null;ois = null;完全有可能在程式執行過程中初始化oos之前就引發了異常,那麼oos,ois還沒有來得及初始化,因此oos,ois根本無需關閉。  
  16.                             oos.close();  
  17.                             ois.close();  
  18. //2.第二種關閉資源的方式(還是不夠安全):假如程式開始已經正常初始化了oos,ois兩個IO流,在關閉oos是出現了異常,那麼程式將在關閉oos時非正常退出,這樣就導致ois得不到關閉,從而導致資源洩漏。為了保證關閉各資源時出現的異常不會相互影響,應該在關閉每個資源時分開使用try catch塊來保證關閉操作不會導致程式非正常退出。  
  19.                             if(oos != null){  
  20.                                      oos.close();  
  21.                             }  
  22.                             if(ois != null){  
  23.                                      ois.close();  
  24.                             }  
  25. //3.第三種關閉資源的方式(比較安全):主要保證一下幾點:  
  26. //(1)使用finally塊來關閉物理資源,保證關閉操作始終會被執行;  
  27. //(2)關閉每個資源之前首先保證引用該資源的引用變數不為null;  
  28. //(3)為每個物理資源使用單獨的trycatch塊關閉資源,保證關閉資源時引發的異常不會影響其他資源的關閉。  
  29.                             if(oos != null){  
  30.                                      try{  
  31.                                                oos.close();  
  32.                                      }catch (Exception ex){  
  33.                                                ex.printStackTrace();  
  34.                                      }  
  35.                             }  
  36.                             if(ois != null){  
  37.                                      try{  
  38.                                                ois.close();  
  39.                                      }catch (Exception ex){  
  40.                                                ex.printStackTrace();  
  41.                                      }  
  42.                             }                 }  
  43.          }  

1.2  finally塊的陷阱

當程式在finally之前使用System.exit(0),finally將不執行。呼叫System.exit(0)將使JVM退出,只要JVM不退出,finally就一定會得到執行。

在java程式執行try塊、catch塊時遇到了return語句,return語句會導致該方法立即結束。統執行完return語句之後,並不會立即結束該方法,而是去尋找該異常處理流程中是否包含Finally塊,如果沒有Finally塊,方法終止,返回相應的返回值。如果有Finally塊,系統立即開始執行Finally塊,只有當Finally執行完成後,系統才會再次跳回來根據return語句結束方法。如果Finally塊使用了return語句來導致方法的結束,則finally塊已經結束了方法,系統不會跳回去執行trycatch裡的任何程式碼

  1. public int test(){  
  2.                    int count = 1;  
  3.                    try{  
  4.                             return ++count;  
  5.                    }finally{  
  6.                             return ++count;  
  7.                    }  
  8.          }  
  9. //以上程式碼最終返回值是:3  
  10. public int test(){  
  11.                    int count = 1;  
  12.                    try{  
  13.                             return ++count;  
  14.                    }finally{  
  15.                             return count++;  
  16.                    }  
  17.          }  
  18. //以上程式碼最終返回值是:2  

throw語句的執行和return語句比較類似。當程式執行trycatch塊遇到throw語句時,throw語句會導致該方法立即結束,系統執行throw語句時並不會立即丟擲異常,而是去尋找該異常處理流程中是否包含finally塊。如果沒有finally塊,程式立即丟擲異常。如果有finally塊,系統立即執行finally塊,只有當finally塊執行完成之後,系統才會再次跳出來丟擲異常。如果finally塊裡使用return語句來結束方法,系統將不會跳回去執行try塊,catch塊去丟擲異常。

例如1:

  1. int count = 1;  
  2.                    try{  
  3.                             throw new RuntimeException("異常1");  
  4.                    }finally{  
  5.                             return count++;  
  6.                    }  
  7. //執行結果:返回值是1,同時不會丟擲任何異常。  

例如2:

  1. int count = 1;  
  2.                    try{  
  3.                             throw new RuntimeException("異常1");  
  4.                    }finally{  
  5.                             throw new RuntimeException("異常2");  
  6.                    }  
  7. //執行結果:Exception in thread "main" java.lang.RuntimeException: 異常2  

1.3  catch塊的用法

1.3.1 catch的順序

對於java的異常捕獲來說,每個try塊至少需要一個catch塊或一個finally塊,絕不能只有單獨一個孤零零try塊。通常情況下,如果try塊被執行一次,則try塊後只有一個catch塊會被執行,絕不可能有多個catch塊被執行。除非在迴圈中使用了continue開始下一次迴圈,下一次迴圈又重新執行了try塊,才可能導致多個catch塊被執行。由於異常處理機制中排在前面的catch(XxxException ex)塊總是會優先獲得執行的機會,因此java對try塊後的多個catch塊的排列順序是有要求的。

   因為java的異常有非常嚴格的繼承體系,許多異常類之間有嚴格的父子關係,比如程式FileNotFoundException異常就是IOException的子類。捕獲父類異常的catch塊都應該排在捕捉子類異常的catch塊之後【先處理小異常(子類異常),在處理大異常(父類異常)】,否則將出現編譯錯誤

         例如以下兩個catch語句不能顛倒順序:

                 

  1. FileInputStream fis = null;  
  2.                   try{  
  3.                            fis = new FileInputStream("a.bin");  
  4.                            fis.read();  
  5.                   }catch(FileNotFoundException ex){  
  6.                            ex.printStackTrace();  
  7.                   }catch(IOException e){  
  8.                            e.printStackTrace();  
  9.                   }  

1.3.2不要用catch代替流程控制

如下邊這個例子:

  1. String[] students = {"liyafang","zhoushilong","luorongbo"};  
  2.                    int i = 0;  
  3.                    while(true){  
  4.                             try{  
  5.                                      System.out.println(students[i++]);  
  6.                             }catch(IndexOutOfBoundsException ex){  
  7.                                      break;  
  8.                             }  
  9.                    }  

這種遍歷陣列的方式不僅難以閱讀,而且執行速度還非常慢。

千萬不要使用異常來進行流程控制。異常機制不是為流程控制而準備的,異常機制知識為程式的意外情況準備的,因此程式只應該為異常情況使用異常機制。所以,不要使用這種“別出心裁”的方法來遍歷陣列。

1.3.3只能catch可能丟擲的異常

  1. /*public static void test1(){ 
  2.                try{ 
  3.                         System.out.println("something"); 
  4.                }catch(IOException e){ 
  5.                         e.printStackTrace(); 
  6.                } 
  7.      } 
  8.      public static void test2(){ 
  9.                try{ 
  10.                         System.out.println("something"); 
  11.                }catch(ClassNotFoundException e){ 
  12.                         e.printStackTrace(); 
  13.                } 
  14.      }*/  

以上程式碼java編譯器是不允許的。

根據java語言規範,如果一個catch子句試圖捕獲一個型別為XxxException的Checked異常時,那麼它對應的try子句必須可能丟擲XxxException或其子類的異常,否則編譯器將提示該程式具有編譯錯誤—但在所有的Checked異常中,Exception是一個異類,無論try塊是怎樣的程式碼,catch(Exception e)總是正確的。

RuntimeException 類及其子類的例項被稱為Runtime異常,不是RuntimeException類及其子類的異常例項則被稱為Checked異常,只要願意,程式設計師總可以使用catch(XxxException ex)來捕獲執行時異常。

總之,程式使用catch捕捉異常時,其實並不能隨心所欲地捕捉所有異常。程式可以在任意想捕捉的地方捕捉RuntimeException異常,Exception,但對於其他的Checked異常,只有當try塊可能丟擲該異常時(try塊中呼叫的某個方法宣告丟擲了該Checked異常),catch塊才捕捉該Checked異常。

 1.3.4實際的修復

如果程式知道如何修復指定異常,應該在catch塊內儘量修復該異常,當該異常情況被修復後可以再次呼叫該方法;如果程式不知道如何修復該異常,也沒有進行任何修復,千萬不要再次呼叫可能導致該異常的方法。

無論如何不要在finally塊中遞迴呼叫可能引起異常的方法,因為這將導致該方法的異常不能被正常丟擲,甚至StackOverflowError錯誤也不能中止程式,只能採用強行結束java.exe程式的方法來中止程式的執行。

1.4  繼承得到的異常

Java語言規定:子類重寫父類的方法時,不能宣告丟擲比父類方法型別更多,範圍更大的異常。也就是說,子類重寫父類方法時,子類方法只能宣告丟擲父類方法所宣告丟擲的異常的子類。

例如:

  1. public interface Type1 {  
  2.          void test() throws ClassNotFoundException;  
  3. }  
  4. public interface Type2 {  
  5.          void test() throws NoSuchMethodException;  
  6. }  
  7. class Test implements Type1, Type2 {  
  8.          @Override  
  9.          public void test() {  
  10.          }  
  11. }  

上面程式碼的異常處理是正確的。

Test實現了Type1介面,實現Type1介面裡的test()方法時可以宣告丟擲ClassNotFoundException異常或該異常的子類,或者不宣告丟擲;Test類實現了Type2介面,實現了Type2介面裡的test()方法時可以宣告丟擲NoSuchMethodException異常或該異常的子類,或者不宣告丟擲。由於Test類同時實現了Type1,Type2兩個介面,因此需要同時實現兩個介面中的test()方法。只能是上面兩種宣告丟擲的交集,不能宣告丟擲任何異常。

原文轉自:Java異常捕捉陷進

相關文章