1. 程式人生 > 實用技巧 >Java學習_異常處理

Java學習_異常處理

  • Java的異常
  1. 計算機程式執行的過程中,總是會出現各種各樣的錯誤。有一些錯誤是使用者造成的,比如,希望使用者輸入一個int型別的年齡,但是使用者的輸入是abc。程式想要讀寫某個檔案的內容,但是使用者已經把它刪除了。還有一些錯誤是隨機出現,並且永遠不可能避免的。比如:

    • 網路突然斷了,連線不到遠端伺服器;
    • 記憶體耗盡,程式崩潰了;
    • 使用者點“列印”,但根本沒有印表機;
    • ……
  2. Java內建了一套異常處理機制,總是使用異常來表示錯誤。異常是一種class,因此它本身帶有型別資訊。異常可以在任何地方丟擲,但只需要在上層捕獲,這樣就和方法呼叫分離了。

     1 try {
     2     String s = processFile(“C:\\test.txt”);
    
    3 // ok: 4 } catch (FileNotFoundException e) { 5 // file not found: 6 } catch (SecurityException e) { 7 // no read permission: 8 } catch (IOException e) { 9 // io error: 10 } catch (Exception e) { 11 // other error: 12 }
  3. Java的異常是class,它的繼承關係如下:
  4. 從繼承關係可知:Throwable是異常體系的根,它繼承自Object
    Throwable有兩個體系:ErrorExceptionError表示嚴重的錯誤,程式對此一般無能為力。
  5. Exception則是執行時的錯誤,它可以被捕獲並處理。
  6. Exception又分為兩大類:

    1. RuntimeException以及它的子類;
    2. RuntimeException(包括IOExceptionReflectiveOperationException等等)
  7. Java規定:
    • 必須捕獲的異常,包括Exception及其子類,但不包括RuntimeException及其子類,這種型別的異常稱為Checked Exception。

    • 不需要捕獲的異常,包括Error及其子類,RuntimeException

      及其子類。

  8. 編譯器對RuntimeException及其子類不做強制捕獲要求,不是指應用程式本身不應該捕獲並處理RuntimeException。是否需要捕獲,具體問題具體分析。
  9. 捕獲異常

    • 捕獲異常使用try...catch語句,把可能發生異常的程式碼放到try {...}中,然後使用catch捕獲對應的Exception及其子類。

       1 import java.io.UnsupportedEncodingException;
       2 import java.util.Arrays;
       3 
       4 public class Main {
       5     public static void main(String[] args) {
       6         byte[] bs = toGBK("中文");
       7         System.out.println(Arrays.toString(bs));
       8     }
       9 
      10     static byte[] toGBK(String s) {
      11         try {
      12             // 用指定編碼轉換String為byte[]:
      13             return s.getBytes("GBK");
      14         } catch (UnsupportedEncodingException e) {
      15             // 如果系統不支援GBK編碼,會捕獲到UnsupportedEncodingException:
      16             System.out.println(e); // 列印異常資訊
      17             return s.getBytes(); // 嘗試使用用預設編碼
      18         }
      19     }
      20 }
    • 如果我們不捕獲UnsupportedEncodingException(去掉try catch),會出現編譯失敗的問題。編譯器會報錯,錯誤資訊類似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,並且準確地指出需要捕獲的語句是return s.getBytes("GBK");。意思是說,像UnsupportedEncodingException這樣的Checked Exception,必須被捕獲。
    • 這是因為String.getBytes(String)方法定義是(如下),在方法定義的時候,使用throws Xxx表示該方法可能丟擲的異常型別(Runtime及其子類除外)。呼叫方在呼叫的時候,必須強制捕獲這些異常,否則編譯器會報錯。
      public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
          ...
      }
    • toGBK()方法中,因為呼叫了String.getBytes(String)方法,就必須捕獲UnsupportedEncodingException。我們也可以不捕獲它,而是在方法定義處用throws表示toGBK()方法可能會丟擲UnsupportedEncodingException,就可以讓toGBK()方法通過編譯器檢查。
       1 import java.io.UnsupportedEncodingException;
       2 import java.util.Arrays;
       3 
       4 public class Main {
       5     public static void main(String[] args) {
       6         byte[] bs = toGBK("中文");
       7         System.out.println(Arrays.toString(bs));
       8     }
       9 
      10     static byte[] toGBK(String s) throws UnsupportedEncodingException {
      11         return s.getBytes("GBK");
      12     }
      13 }
    • 只要是方法宣告的Checked Exception,不在呼叫層捕獲,也必須在更高的呼叫層捕獲。所有未捕獲的異常,最終也必須在main()方法中捕獲,不會出現漏寫try的情況。這是由編譯器保證的。main()方法也是最後捕獲Exception的機會。
    • 如果是測試程式碼,上面的寫法就略顯麻煩。如果不想寫任何try程式碼,可以直接把main()方法定義為throws Exception。因為main()方法聲明瞭可能丟擲Exception,也就聲明瞭可能丟擲所有的Exception,因此在內部就無需捕獲了。代價就是一旦發生異常,程式會立刻退出。
    • toGBK()內部“消化”異常
      static byte[] toGBK(String s) {
          try {
              return s.getBytes("GBK");
          } catch (UnsupportedEncodingException e) {
              // 什麼也不幹
          }
          return null;
    • 這種捕獲後不處理的方式是非常不好的,即使真的什麼也做不了,也要先把異常記錄下來:
      static byte[] toGBK(String s) {
          try {
              return s.getBytes("GBK");
          } catch (UnsupportedEncodingException e) {
              // 先記下來再說:
              e.printStackTrace();
          }
          return null;
    • 所有異常都可以呼叫printStackTrace()方法列印異常棧,這是一個簡單有用的快速列印異常的方法。
  • 捕獲異常
    1. 凡是可能丟擲異常的語句,都可以用try ... catch捕獲。把可能發生異常的語句放在try { ... }中,然後使用catch捕獲對應的Exception及其子類。  
    2. 可以使用多個catch語句,每個catch分別捕獲對應的Exception及其子類。JVM在捕獲到異常後,會從上到下匹配catch語句,匹配到某個catch後,執行catch程式碼塊,然後不再繼續匹配。

      簡單地說就是:多個catch語句只有一個能被執行。

    3. 存在多個catch的時候,catch的順序非常重要:子類必須寫在前面。
    4. 無論是否有異常發生,都希望執行一些語句,例如清理工作,可以把執行語句寫若干遍:正常執行的放到try中,每個catch再寫一遍。

       1 public static void main(String[] args) {
       2     try {
       3         process1();
       4         process2();
       5         process3();
       6         System.out.println("END");
       7     } catch (UnsupportedEncodingException e) {
       8         System.out.println("Bad encoding");
       9         System.out.println("END");
      10     } catch (IOException e) {
      11         System.out.println("IO error");
      12         System.out.println("END");
      13     }
      14 }
    5. Java的try ... catch機制還提供了finally語句,finally語句塊保證有無錯誤都會執行。上述程式碼可以改寫如下。
      public static void main(String[] args) {
          try {
              process1();
              process2();
              process3();
          } catch (UnsupportedEncodingException e) {
              System.out.println("Bad encoding");
          } catch (IOException e) {
              System.out.println("IO error");
          } finally {
              System.out.println("END");
          }
      }
    6. finally有幾個特點:

      1. finally語句不是必須的,可寫可不寫;
      2. finally總是最後執行。
    7. 某些情況下,可以沒有catch,只使用try ... finally結構。
      void process(String file) throws IOException {
          try {
              ...
          } finally {
              System.out.println("END");
          }
      }

      因為方法聲明瞭可能丟擲的異常,所以可以不寫catch

  • 丟擲異常
    1. 檢視Integer.java原始碼可知,丟擲異常的方法程式碼如下。
      public static int parseInt(String s, int radix) throws NumberFormatException {
          if (s == null) {
              throw new NumberFormatException("null");
          }
          ...
      }
    2. 檢視Integer.java原始碼可知,丟擲異常的方法程式碼如下當發生錯誤時,例如,使用者輸入了非法的字元,我們就可以丟擲異常。如何丟擲異常?參考Integer.parseInt()方法,丟擲異常分兩步:

      1. 建立某個Exception的例項;
      2. throw語句丟擲。
        void process2(String s) {
            if (s==null) {
                NullPointerException e = new NullPointerException();
                throw e;
            }
        }
        
        或:
        
        void process2(String s) {
            if (s==null) {
                throw new NullPointerException();
            }
        }
      3. 在程式碼中獲取原始異常可以使用Throwable.getCause()方法。如果返回null,說明已經是“根異常”了。
      4. 捕獲到異常並再次丟擲時,一定要留住原始異常,否則很難定位第一案發現場!
    3. try或者catch語句塊中丟擲異常,不會影響finally的執行。JVM會先執行finally,然後丟擲異常。
    4. 如果在執行finally語句時丟擲異常,那麼,finally丟擲異常後,原來在catch中準備丟擲的異常就“消失”了,因為只能丟擲一個異常。沒有被丟擲的異常稱為“被遮蔽”的異常(Suppressed Exception)。
      public class Main {
          public static void main(String[] args) {
              try {
                  Integer.parseInt("abc");
              } catch (Exception e) {
                  System.out.println("catched");
                  throw new RuntimeException(e);
              } finally {
                  System.out.println("finally");
                  throw new IllegalArgumentException();
              }
          }
      }
      
      輸出:
      
      catched
      finally
      Exception in thread "main" java.lang.IllegalArgumentException
      at Main.main(Main.java:11)
    5. 在極少數的情況下,我們需要獲知所有的異常。如何儲存所有的異常資訊?方法是先用origin變數儲存原始異常,然後呼叫Throwable.addSuppressed(),把原始異常新增進來,最後在finally丟擲。(通過Throwable.getSuppressed()可以獲取所有的Suppressed Exception。絕大多數情況下,在finally中不要丟擲異常。因此,通常不需要關心Suppressed Exception。)
       1 public class Main {
       2     public static void main(String[] args) throws Exception {
       3         Exception origin = null;
       4         try {
       5             System.out.println(Integer.parseInt("abc"));
       6         } catch (Exception e) {
       7             origin = e;
       8             throw e;
       9         } finally {
      10             Exception e = new IllegalArgumentException();
      11             if (origin != null) {
      12                 e.addSuppressed(origin);
      13             }
      14             throw e;
      15         }
      16     }
      17 }

  • 自定義異常

  晚上接著更