很多程序員多年都沒掌握的異常處理技巧和原則


Java中的異常機制是指:當程序在運行過程中遇到意外情況時會自動拋出一個Exception對象來通知程序,程序收到這個異常通知后可以采取各種處理措施,這種機制能使程序更加健壯,可讀性更高。本文就來講講異常處理的相關知識。


異常分類

Java中的異常分為RuntimeException和CheckedException。

  • RuntimeException:程序運行過程中出現錯誤,才會被檢查的異常。例如:類型錯誤轉換,數組下標訪問越界,空指針異常、找不到指定類等等。
  • CheckedException:來自於Exception且非運行時異常都是檢查異常,編譯器會強制檢查並通過try-catch塊來對其捕獲,或者在方法頭聲明拋出該異常,交給調用者處理。常見的checked異常有FileNotFoundExceptionInterruptedException等。

Error和Exception的區別

在談到Exception時,經常會涉及到Error。Error和Exception存在如下的區別:

Error是指系統中的錯誤,程序員是不能改變和處理的,是在程序運行時出現的錯誤,只能通過修改程序才能修正。Java中的Error一般是指與虛擬機相關的問題,如系統崩潰,虛擬機錯誤,內存空間不足,方法調用棧溢出等。對於這類錯誤的導致的應用程序中斷,僅靠程序本身無法恢復和和預防,遇到這樣的錯誤,建議讓程序終止,調整代碼或者虛擬機參數再重新啟動程序;

Exception(異常)是程序可以處理的。遇到這類異常,程序員應該盡可能捕獲處理異常,使程序恢復運行,而不應該隨意終止異常。實在不知道如何處理就向上拋出該異常留給調用者處理。

總結下:異常(Exception)是一種非程序原因的操作失敗(Failure),而錯誤(Error)則意味着程序有缺陷(Bug)。

異常處理的原則

1. 能處理的異常盡早處理
對於能明確知道要怎么處理的異常要第一時間處理掉。對於不知道要怎么處理的異常,要么直接向上拋出,要么轉換成RuntimeException再向上拋出,讓調用者處理。

2. 具體明確原則
盡量不要用Exception捕獲拋出所有異常。拋出異常時需要針對具體問題來拋出異常,拋出的異常要足夠具體詳細;在捕獲異常時需要對捕獲的異常進行細分,這時會有多個catch語句塊,這幾個catch塊中間泛化程度越低的異常需要越放在前面捕獲,泛化程度高的異常捕獲放在后面,這樣的好處是如果出現異常可以近可能得明確異常的具體類型是什么。

拋出時:


public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    //根據不同的情況拋出不同的異常
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    ...
}

捕獲時:

public void foo1(String fileName){
    File file = new File(fileName);
    InputStream in = null;
    try {
        in = new FileInputStream(file);
        Integer num = in.read();
        in.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e){
        e.printStackTrace();
    } catch(Exception e){
        e.printStackTrace();
    }
}

3. 系統需要有統一異常處理機制
系統需要有自己的一套統一異常處理的機制,如果系統使用Spring等框架的話可以非常簡單地引入統一異常處理框架。另外在統一異常處理時一定要打出異常堆棧,不然的話問題可能就無從查起了。

4. catch處理異常時先打印異常棧,再做其他處理

 public static void main(String[] args) {
      try {
          String s = null;
          s.length();
      } catch (Exception e) {
          int a = 3/0;
          logger.error("ee", e);
      }
}

上面的代碼中,我們本意是想catch住空指針異常並打印異常棧。但是我們處理時並沒有先打印異常棧,在打印之前又出現了一個除零異常,這個異常會覆蓋之前的空指針,讓我們不好定位原始的異常。

上面的異常輸出如下:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.pingan.abses.service.xxxx.main(xxx.java:1573)

所以catch異常時要先打印異常棧。

  public static void main(String[] args) {
      try {
          String s = null;
          s.length();
      } catch (Exception e) {
          logger.error("ee", e);
          //其他處理措施
      }
  }

在 finally 中處理時也會類似屏蔽原始異常的問題,我們開發時需要注意

try {
        int a = 3 / 0;
  } catch (Exception e) {
      throw new RuntimeException("除零異常");
  } finally {
      //做些其他操作,但是發生了異常
      logger.info("some other op..");
      throw new RuntimeException("未知異常");
}

5. 一些其他注意點

  • try語句塊內要分清穩定代碼和非穩定代碼,對於穩定的不會出現異常的代碼不要放到try語句塊中;
  • catch捕獲的異常一定要處理,吃掉異常不處理的話將是滅頂之災;
  • finally中不要使用return語句,因為finally語句塊最后一定會執行,這里的return語句會覆蓋之前的return語句

如何自定義異常

在復雜業務環境下,java自帶的異常可能滿足不了我們業務的需求, 這個時候我們可以自定義異常來進行對業務異常的處理。

public class MyException extends RuntimeException {

    private static final long serialVersionUID = 6958499248468627021L;
   
    private String errorCode;
    
    private String errorMsg;

    public MyException(String errorCode,String errorMsg){
        super(errorMsg);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public MyException(String errorCode,String errorMsg,Throwable throwable){
        super(errorCode,throwable);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public String getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
}

在使用MyException時,最好定義一個枚舉類用來枚舉錯誤代碼和錯誤詳情。

return和finally的執行順序

在Java中,異常處理主要由try、catch、throw、throws和finally5個關鍵字處理。Java程序中如果出現了異常,而且沒有被try-catch塊捕獲的話,系統會把異常一直往上層拋,直到遇到處理代碼。如果一直沒有處理塊,就拋到最上層,如果是多線程就由Thread.run()拋出,如果是單線程就被main()拋出。拋出之后,如果是子線程,這個子線程就退出了。如果是主程序拋出的異常,那么這整個程序也就退出了。

一個比較常見的異常處理流程如下:

    try {
        //step1
        System.out.println("try...");
        throw new RuntimeException("異常1...");
    }catch (Exception e){
        //step2
        System.out.println("catch。。。");
    }finally {
        //step3
        System.out.println("finally。。。");
    }
    //step4
    System.out.println("end...");

上述的代碼中由於拋出的異常被順利catch住了,所以當前線程不會結束,程序會繼續往下執行,step4這步代碼會被打印出來。

	try {
	    System.out.println("try...");
	    throw new RuntimeException("異常1...");
	}catch (Exception e){
	    System.out.println("catch。。。");
	    throw new RuntimeException("異常2...");
	}finally {
	    System.out.println("finally。。。");
	}
	System.out.println("end...");

上面的代碼中,由於catch塊中又拋出了一個異常,而這個異常沒有相應的catch塊處理,所以系統會向上拋這個異常,最后的打印語句也就的不到執行。

try、catch、finally、throw和throws使用歸納

  • try、catch和finally都不能單獨使用,只能是try-catch、try-finally或者try-catch-finally。
  • try語句塊監控代碼,出現異常就停止執行下面的代碼,然后將異常移交給catch語句塊來處理,catch塊執行完之后代碼還會繼續往下執行
  • finally語句塊中的代碼一定會被執行,常用於回收資源 。
  • throws:聲明一個異常,告知方法調用者。
  • throw :拋出一個異常,至於該異常被捕獲還是繼續拋出都與它無關。

還有一個比較重要的是return和finally的執行關系,可以參考下這篇博客

異常鏈

在平時的開發中,常常會在捕獲一個異常后拋出另外一個自定義異常,並且希望把異常原始信息保存下來,這被稱為異常鏈。我們在自定義異常時,只要提供一個接收throwable參數的構造函數即可:

public MyException(String errorCode,String errorMsg,Throwable cause){
    super(errorCode,cause);
    this.errorCode = errorCode;
    this.errorMsg = errorMsg;
}

try-with-resources

我們知道,在Java編程過程中,如果打開了外部資源(文件、數據庫連接、網絡連接等),我們必須在這些外部資源使用完畢后,手動關閉它們。因為外部資源不由JVM管理,無法享用JVM的垃圾回收機制,如果我們不在編程時確保在正確的時機關閉外部資源,就會導致外部資源泄露,緊接着就會出現文件被異常占用,數據庫連接過多導致連接池溢出等諸多很嚴重的問題。

  1. 傳統的資源關閉方式
//這種方式關閉資源,代碼顯得比較臃腫
public static void main(String[] args) {
    FileInputStream inputStream = null;
    try {
        inputStream = new FileInputStream(new File("test"));
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    }
}
  1. try-with-resources方式關閉資源
public static void main(String[] args) {
    try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

將外部資源的句柄對象的創建放在try關鍵字后面的括號中,當這個try-catch代碼塊執行完畢后,Java會確保外部資源的close方法被調用。代碼是不是瞬間簡潔許多!當一個外部資源的句柄對象實現了AutoCloseable接口,JDK7中便可以利用try-with-resource語法更優雅的關閉資源,消除板式代碼。

這種方式其實是一種語法糖,關於語法糖的詳細介紹可以我的博客

公眾號推薦

歡迎大家關注我的微信公眾號「程序員自由之路」

參考


免責聲明!

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



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