(一)異常處理機制詳解
# 前言
本文主要是對Java異常處理機制的闡述,瞭解Java的異常機制的設計和分類,及Java異常有哪些坑,如何在自定義異常類時避免採坑。
# 異常機制分類
異常情況是指阻止當前方法或作用域繼續繼續執行的情況。在Java中異常也是物件,我們可以像建立其他物件一樣,用new在堆上建立異常物件。從上圖可以看到Throwable是所有異常型別的根類,它有兩個重要的子類:Exception和Error。
- Error(錯誤)
Error表示編譯時和系統錯誤(除特殊情況外我們無需關注),比如程式碼允許是JVM執行錯誤,或記憶體不足時OutOfMemoryError。
- Exception(異常)
Exception是可以丟擲/處理的異常。在Java類庫、使用者方法及執行時故障都可能丟擲Exception型別異常,我們程式設計師需要關注的主要是Exception。它又分為執行時異常和非執行時異常。
執行時異常:由RuntimeException和其子類異常組成。比如NullPointerException(空指標異常)、IndexOutOfBoundsException(下標越界異常)。這些異常通常是非受檢異常,可以捕獲處理或者不處理。一般有程式邏輯引起的。執行時異常的特點是Java編譯器編譯時不會檢查它,就算有這種異常編譯也能通過,究其原因,RuntimeException代表的是程式設計錯誤。
非執行時異常:包括RuntimeException以外的異常,型別上都屬於Exception類及其子類。從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如IOException、SQLException等以及使用者自定義的Exception異常。
# try-catch-finally捕獲異常
在Java中使用try-catch或者try-catch-finally捕獲異常。
## try塊
對於有可能出現異常情況的程式碼塊Code,我們可以把它放在try塊裡。
try { //可能發生異常的程式碼塊 }
## 異常處理程式
異常處理程式必須緊跟在try塊後,以關鍵字catch表示。當異常被丟擲時,異常處理機制將負責搜尋引數與異常型別相匹配的第一個程式,然後進入catch子程式執行。
try { //可能發生異常的程式碼塊 } catch (Type1 id1) { //捕獲並處理異常型別為Type1的異常 } catch (Type2 id2) { //捕獲並處理異常型別為Type1的異常 } finally { //無論如何都會走到的程式碼 //有如下極端情況不會走到finally程式碼塊,但一般不考慮 //比如CPU掉電、執行緒異常終止等 } // etc...
有時也可以採用maltiple catch,具體參考以下程式碼段。
## throw和throws
我們在程式設計時,需要針對某種異常情況丟擲異常給客戶端,程式碼如下
if (s == null) { throw new NullPointerException(); }
throws是一種“異常說明”方式,它屬於方法宣告的一部分,跟在形式引數列表之後。
void func() throws Exception1, Exception2 {}
這種異常說明的方式,可以強制函式使用者強制處理該異常情況。在定義抽象基類和介面時這種能力很重要,這樣派生類就可以處理這些預先宣告的異常。
從上面可以看出throw主要是用來中斷程式執行並移交異常物件到執行時處理。throws用於宣告方法可丟擲的異常,是異常說明的一種機制。
## 使用finally做清理工作
對於一些程式碼,希望無論try塊是否有異常丟擲,都能得到執行,比如開啟的檔案控制代碼或者網路連線,可以使用try-catch-finally,程式碼如下所示。
public class FinallyWorks { static int count = 0; public static void main(String[] args) { while (true) { try { // count為0時拋異常 if (count ++ == 0) { throw new IOException(); } System.out.println("No exception"); } catch (Exception e) {//該句可以捕獲所有異常 System.out.println("IOException"); } finally { System.out.println("In finally clause"); if (count == 2) break; } } } }
/** * Output **/ IOException In finally clause No exception In finally clause
## 新特性
### multiple exception
如果一個try塊中有多個異常要被捕獲,catch塊中的程式碼會變醜陋的同時還要用多餘的程式碼來記錄異常。有鑑於此,Java 7的一個新特徵是:一個catch子句中可以捕獲多個異常。示例程式碼如下:
catch(IOException | SQLException | Exception ex){ log.warn(ex); throw new MyException(ex.getMessage()); }
### try-with-resources
try-with-resources[1][2] 語句會確保在try語句結束時關閉所有資源。實現了java.lang.AutoCloseable或java.io.Closeable的物件都可以做為資源。使用try-with-resources進行資源的自動關閉,在try子句中能建立一個資源物件,當程式的執行完try-catch之後,執行環境自動關閉資源。示例程式碼如下:
/** * code 1 **/ try (FileInputStream fis = new FileInputStream("example.java")) { // line 1 System.out.println("fis created in try-with-resources"); doSomething(); // line2 } catch (Exception e) { e.printStackTrace(); }
在Java7之前我們使用finally進行資源的關閉,如下所示
/** * code 2 **/ FileInputStream fis = new FileInputStream("example.java"); try { System.out.println("fis created in try-with-resources"); doSomething(); // line 3 } catch (Exception e) { e.printStackTrace(); } finally { if (fis != null) { fis.close();// line 4 } }
異常遮蔽請參見參考文獻[1][2]
# 正確的使用異常
## 不要在finally中使用return關鍵字。
finally塊中return返回後方法結束執行,會覆蓋try塊中的return語句,換句話說就是遮蔽了try塊中的return語句。
/** * @author liangk * @date 18/09/2018 */ public class FinallyReturn { public static void main(String[] args) { String result = finallyReturnTest(); System.out.println(result); } public static String finallyReturnTest() { try { System.out.println("finallyReturnTest start"); String result = "Hello EveryBody!"; return result; } finally { return "The finally block will be printed in the end"; } } }
/** * Output */ finallyReturnTest start The finally block will be printed in the end
## finally 塊必須對資源物件、流物件進行關閉。
如果JKD7及以上版本,可以使用上文介紹的try-with-resources方式
## 避免直接丟擲RuntimeException及其子類。
更不允許丟擲Exception或Throwable(建議丟擲具體的異常物件)
## 建議採用預檢查方式規避RuntimeException異常,而不應該catch的方式處理。
public void readPreferences(String fileName) { InputStream in = new FileInputStream(fileName); }
上面的程式如果fileName是null,就會丟擲NullPointerException,由於沒有第一時間暴露問題,堆疊資訊費解,需要相對複雜的定位。如果我們採取下面的方式,就很容易解決問題
public void readPreferences(String fileName) { Objects.requireNonNull(fileName); InputStream in = new FileInputStream(fileName); }
## 不允許直接吞沒異常。
直接吞沒異常,既不處理也不丟擲,可能會導致難以診斷的異常情況,無法判斷異常從哪裡結束,什麼原因產生的異常情況。
## try塊只包含可能會出現異常的必要程式碼段。- try-catch 程式碼段會產生額外的效能開銷,或者換個角度說,它往往會影響JVM對程式碼進行優化,所以建議僅捕獲必要的程式碼段,不能包住整段程式碼。- Java每例項化一個Exception,都會對當時的棧進行快照,這是一個相對較重的操作。如果異常頻繁發生,開銷就無法忽略。
## 不允許使用異常實現流程控制和條件控制。
我們可以利用break\continue \if else 配合finally實現流程控制。但利用異常控制流程,比通常意義上的條件語句(if/else、switch)要低效。
## try塊放到事務程式碼中,catch異常後,如果需要回滾事務,一定注意手動回滾事務。
# 參考文獻
[1] [詳解try-with-resource](http://www.oracle.com/technetwork/cn/articles/java/trywithresources-401775-zhs.html) [2] [try-with-resource官方文件](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html)