JAVA 異常類型結構分析


JAVA 異常類型結構分析

Throwable 是所有異常類型的基類,Throwable 下一層分為兩個分支,Error 和 Exception.

 

 

Error Exception

  • Error

  Error 描述了 JAVA 程序運行時系統的內部錯誤,通常比較嚴重不可挽回,除了通知用戶和盡力使應用程序安全地終止之外,無能為力,應用程序不應該嘗試去捕獲這種異常。通常為一些虛擬機異常,如 StackOverflowError 等。

  • Exception

  Exception 類型下面又分為兩個分支,一個分支派生自 RuntimeException,這種異常通常為程序錯誤導致的異常,在運行時拋出異常,在編譯時不受檢查;另一個分支為非派生自 RuntimeException 的異常,這種異常通常是程序本身沒有問題,由於像 I/O 錯誤等問題導致的異常,每個異常類用逗號隔開,在編譯時被檢查。

受查異常和非受查異常

  • 受查異常

  受查異常會在編譯時被檢測。如果一個方法中的代碼會拋出受查異常,則該方法必須包含異常處理,即 try-catch 代碼塊,或在方法簽名中用 throws 關鍵字聲明該方法可能會拋出的受查異常,否則編譯無法通過。如果一個方法可能拋出多個受查異常類型,就必須在方法的簽名處列出所有的異常類或者這些異常的相同父類。

通過 throws 關鍵字聲明可能拋出的異常

private static void readFile(String filePath) throws IOException {
  File file = new File(filePath);
  String result;
  BufferedReader reader = new BufferedReader(new FileReader(file));
  while((result = reader.readLine())!=null) {
      System.out.println(result);
  }
  reader.close();
}

try-catch 處理異常

private static void readFile(String filePath) {
  File file = new File(filePath);
  String result;
  BufferedReader reader;
  try {
      reader = new BufferedReader(new FileReader(file));
      while((result = reader.readLine())!=null) {
          System.out.println(result);
      }
      reader.close();
  } catch (IOException e) {
      e.printStackTrace();
  }
}
  • 非受查異常

  非受查異常不會在編譯時被檢測。JAVA 中 Error 和 RuntimeException 類的子類屬於非受查異常,除此之外繼承自 Exception 的類型為受查異常。

異常的拋出與捕獲

直接拋出異常

  通常,應該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續傳遞下去。傳遞異常可以在方法簽名處使用 throws 關鍵字聲明可能會拋出的異常。

private static void readFile(String filePath) throws IOException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = new BufferedReader(new FileReader(file));
    while((result = reader.readLine())!=null) {
        System.out.println(result);
    }
    reader.close();
}

封裝異常再拋出

有時我們會從 catch 中拋出一個異常,目的是為了改變異常的類型。多用於在多系統集成時,當某個子系統故障,異常類型可能有多種,可以用統一的異常類型向外暴露,不需暴露太多內部異常細節。

private static void readFile(String filePath) throws MyException {    
    try {
        // code
    } catch (IOException e) {
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    }
}

捕獲異常

  在一個 try-catch 語句塊中可以捕獲多個異常類型,並對不同類型的異常做出不同的處理

private static void readFile(String filePath) {
    try {
        // code
    } catch (FileNotFoundException e) {
        // handle FileNotFoundException
    } catch (IOException e){
        // handle IOException
    }
}

同一個 catch 也可以捕獲多種類型異常,用 | 隔開

private static void readFile(String filePath) {
    try {
        // code
    } catch (FileNotFoundException | UnknownHostException e) {
        // handle FileNotFoundException or UnknownHostException
    } catch (IOException e){
        // handle IOException
    }
}

自定義異常

  定義一個異常類應包含兩個構造函數,一個無參構造函數和一個帶有詳細描述信息的構造函數(Throwable 的 toString 方法會打印這些詳細信息,調試時會用到)

public class MyException extends Exception {
    public MyException(){ }
    public MyException(String msg){
        super(msg);
    }
    // ...
}

try-catch-finally

 

  當方法中發生異常,異常處之后的代碼不會再執行,如果之前獲取了一些本地資源需要釋放,則需要在方法正常結束時和 catch 語句中都調用釋放本地資源的代碼,顯得代碼比較繁瑣,finally 語句可以解決這個問題。

private static void readFile(String filePath) throws MyException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(file));
        while((result = reader.readLine())!=null) {
            System.out.println(result);
        }
    } catch (IOException e) {
        System.out.println("readFile method catch block.");
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    } finally {
        System.out.println("readFile method finally block.");
        if (null != reader) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

  調用該方法時,讀取文件時若發生異常,代碼會進入 catch 代碼塊,之后進入 finally 代碼塊;若讀取文件時未發生異常,則會跳過 catch 代碼塊直接進入 finally 代碼塊。所以無論代碼中是否發生異常,fianlly 中的代碼都會執行。

  若 catch 代碼塊中包含 return 語句,finally 中的代碼還會執行嗎?將以上代碼中的 catch 子句修改如下

catch (IOException e) {
    System.out.println("readFile method catch block.");
    return;
}

  測試可知,即使 catch 中包含了 return 語句,finally 子句依然會執行。若 finally 中也包含 return 語句,finally 中的 return 會覆蓋前面的 return.

try-with-resource

  上面例子中,finally 中的 close 方法也可能拋出 IOException, 從而覆蓋了原始異常。JAVA 7 提供了更優雅的方式來實現資源的自動釋放,自動釋放的資源需要是實現了 AutoCloseable 接口的類。

private  static void tryWithResourceTest(){
    try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
        // code
    } catch (IOException e){
        // handle exception
    }
}

  try 代碼塊退出時,會自動調用 scanner.close 方法,和把 scanner.close 方法放在 finally 代碼塊中不同的是,若 scanner.close 拋出異常,則會被抑制,拋出的仍然為原始異常。被抑制的異常會由 addSusppressed 方法添加到原來的異常,如果想要獲取被抑制的異常列表,可以調用 getSuppressed 方法來獲取。

阿里巴巴異常處理規約

  • 【強制】 Java 類庫中定義的可以通過預檢查方式規避的 RuntimeException 異常不應該通過
    catch 的方式來處理,比如: NullPointerException, IndexOutOfBoundsException 等等。
    說明: 無法通過預檢查的異常除外,比如,在解析字符串形式的數字時,不得不通過 catch
    NumberFormatException 來實現。
    正例: if (obj != null) {…}
    反例: try { obj.method(); } catch (NullPointerException e) {…}
  • 【強制】 異常不要用來做流程控制,條件控制。
    說明: 異常設計的初衷是解決程序運行中的各種意外情況,且異常的處理效率比條件判斷方式
    要低很多。
  • 【強制】 catch 時請分清穩定代碼和非穩定代碼,穩定代碼指的是無論如何不會出錯的代碼。
    對於非穩定代碼的 catch 盡可能進行區分異常類型,再做對應的異常處理。
    說明: 對大段代碼進行 try-catch,使程序無法根據不同的異常做出正確的應激反應,也不利
    於定位問題,這是一種不負責任的表現。
    正例: 用戶注冊的場景中,如果用戶輸入非法字符, 或用戶名稱已存在, 或用戶輸入密碼過於
    簡單,在程序上作出分門別類的判斷,並提示給用戶。
  • 【強制】 捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請
    將該異常拋給它的調用者。最外層的業務使用者,必須處理異常,將其轉化為用戶可以理解的
    內容。
  • 【強制】 有 try 塊放到了事務代碼中, catch 異常后,如果需要回滾事務,一定要注意手動回
    滾事務。
  • 【強制】 finally 塊必須對資源對象、流對象進行關閉,有異常也要做 try-catch。
    說明: 如果 JDK7 及以上,可以使用 try-with-resources 方式。
  • 【強制】 不要在 finally 塊中使用 return。
    說明: finally 塊中的 return 返回后方法結束執行,不會再執行 try 塊中的 return 語句。
  • 【強制】 捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。
    說明: 如果預期對方拋的是綉球,實際接到的是鉛球,就會產生意外情況。
  • 【推薦】 方法的返回值可以為 null,不強制返回空集合,或者空對象等,必須添加注釋充分
    說明什么情況下會返回 null 值。
    說明: 本手冊明確防止 NPE 是調用者的責任。即使被調用方法返回空集合或者空對象,對調用者來說,也並非高枕無憂,必須考慮到遠程調用失敗、 序列化失敗、 運行時異常等場景返回
    null 的情況。
  • 【推薦】 防止 NPE,是程序員的基本修養,注意 NPE 產生的場景:
    1)返回類型為基本數據類型, return 包裝數據類型的對象時,自動拆箱有可能產生 NPE。
    反例: public int f() { return Integer 對象}, 如果為 null,自動解箱拋 NPE。
    2) 數據庫的查詢結果可能為 null。
    3) 集合里的元素即使 isNotEmpty,取出的數據元素也可能為 null。
    4) 遠程調用返回對象時,一律要求進行空指針判斷,防止 NPE。
    5) 對於 Session 中獲取的數據,建議 NPE 檢查,避免空指針。
    6) 級聯調用 obj.getA().getB().getC(); 一連串調用,易產生 NPE。
    正例: 使用 JDK8 的 Optional 類來防止 NPE 問題。
  • 【推薦】 定義時區分 unchecked / checked 異常,避免直接拋出 new RuntimeException(),
    更不允許拋出 Exception 或者 Throwable,應使用有業務含義的自定義異常。推薦業界已定義
    過的自定義異常,如: DAOException / ServiceException 等。
  • 【參考】 對於公司外的 http/api 開放接口必須使用“錯誤碼”; 而應用內部推薦異常拋出;
    跨應用間 RPC 調用優先考慮使用 Result 方式,封裝 isSuccess()方法、 “錯誤碼”、 “錯誤簡
    短信息”。
    說明: 關於 RPC 方法返回方式使用 Result 方式的理由:
    1) 使用拋異常返回方式,調用方如果沒有捕獲到就會產生運行時錯誤。
    2) 如果不加棧信息,只是 new 自定義異常,加入自己的理解的 error message,對於調用
    端解決問題的幫助不會太多。如果加了棧信息,在頻繁調用出錯的情況下,數據序列化和傳輸
    的性能損耗也是問題。
  • 【參考】 避免出現重復的代碼(Don’t Repeat Yourself) ,即 DRY 原則。
    說明: 隨意復制和粘貼代碼,必然會導致代碼的重復,在以后需要修改時,需要修改所有的副
    本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是組件化。
    正例: 一個類中有多個 public 方法,都需要進行數行相同的參數校驗操作,這個時候請抽取:
    private boolean checkParam(DTO dto) {…}

常見面試題

  • Error 和 Exception 區別是什么?
    Error 類型的錯誤通常為虛擬機相關錯誤,如系統崩潰,內存不足,堆棧溢出等,編譯器不會對這類錯誤進行檢測,JAVA 應用程序也不應對這類錯誤進行捕獲,一旦這類錯誤發生,通常應用程序會被終止,僅靠應用程序本身無法恢復;

   Exception 類的錯誤是可以在應用程序中進行捕獲並處理的,通常遇到這種錯誤,應對其進行處理,使應用程序可以繼續正常運行。

  • 運行時異常和一般異常區別是什么?
    編譯器不會對運行時異常進行檢測,沒有 try-catch,方法簽名中也沒有 throws 關鍵字聲明,編譯依然可以通過。如果出現了 RuntimeException, 那一定是程序員的錯誤。

     一般一場如果沒有 try-catch,且方法簽名中也沒有用 throws 關鍵字聲明可能拋出的異常,則編譯無法通過。這類異常通常為應用環境中的錯誤,即外部錯誤,非應用程序本身錯誤,如文件找不到等。

  • NoClassDefFoundError 和 ClassNotFoundException 區別是什么?
    NoClassDefFoundError 是一個 Error 類型的異常,是由 JVM 引起的,不應該嘗試捕獲這個異常。引起該異常的原因是 JVM 或 ClassLoader 嘗試加載某類時在內存中找不到該類的定義,該動作發生在運行期間,即編譯時該類存在,但是在運行時卻找不到了,可能是變異后被刪除了等原因導致;

   ClassNotFoundException 是一個受查異常,需要顯式地使用 try-catch 對其進行捕獲和處理,或在方法簽名中用 throws 關鍵字進行聲明。當使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 動態加載類到內存的時候,通過傳入的類路徑參數沒有找到該類,就會拋出該異常;另一種拋出該異常的可能原因是某個類已經由一個類加載器加載至內存中,另一個加載器又嘗試去加載它。

  • JVM 是如何處理異常的?
    在一個方法中如果發生異常,這個方法會創建一個異常對象,並轉交給 JVM,該異常對象包含異常名稱,異常描述以及異常發生時應用程序的狀態。創建異常對象並轉交給 JVM 的過程稱為拋出異常。可能有一系列的方法調用,最終才進入拋出異常的方法,這一系列方法調用的有序列表叫做調用棧。
    JVM 會順着調用棧去查找看是否有可以處理異常的代碼,如果有,則調用異常處理代碼。當 JVM 發現可以處理異常的代碼時,會把發生的異常傳遞給它。如果 JVM 沒有找到可以處理該異常的代碼塊,JVM 就會將該異常轉交給默認的異常處理器(默認處理器為 JVM 的一部分),默認異常處理器打印出異常信息並終止應用程序。
  • throw 和 throws 的區別是什么?
    throw 關鍵字用來拋出方法或代碼塊中的異常,受查異常和非受查異常都可以被拋出。
    throws 關鍵字用在方法簽名處,用來標識該方法可能拋出的異常列表。一個方法用 throws 標識了可能拋出的異常列表,調用該方法的方法中必須包含可處理異常的代碼,否則也要在方法簽名中用 throws 關鍵字聲明相應的異常。
  • 常見的 RuntimeException 有哪些?
  1. ClassCastException(類轉換異常)
  2. IndexOutOfBoundsException(數組越界)
  3. NullPointerException(空指針)
  4. ArrayStoreException(數據存儲異常,操作數組時類型不一致)
  5. 還有IO操作的BufferOverflowException異常

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM