處理異常是一門學問嗎?

在這裏插入圖片描述
01、

你有沒有這樣的印象,當你想要更新一款 APP 的時候,它的更新日誌裏總有這麼一兩句描述:

修復若干 bug
殺了某程序員祭天,併成功解決掉他遺留的 bug
作爲一名負責任的程序員,我們當然希望程序不會出現 bug,因爲 bug 出現的越多,間接地證明了我們的編程能力越差,至少領導是這麼看的。

事實上,領導是不會拿自己的腦袋宣言的:“我們的程序絕不存在任何一個 bug。”但當程序出現 bug 的時候,領導會毫不猶豫地選擇讓程序員背鍋。

爲了讓自己少背鍋,我們可以這樣做:

在編碼階段合理使用異常處理機制,並記錄日誌以備後續分析
在測試階段進行大量有效的測試,在用戶發現錯誤之前發現錯誤
還有一點需要做的是,在敲代碼之前,學習必要的編程常識,做到兵馬未動,糧草先行。

02、

在 Java 中,異常(Throwable)的層次結構大致如下。
在這裏插入圖片描述
Error 類異常描述了 Java 運行時系統的內部錯誤,比如最常見的 OutOfMemoryError 和 NoClassDefFoundError。

導致 OutOfMemoryError 的常見原因有以下幾種:

內存中加載的數據量過於龐大,如一次從數據庫取出過多數據;
集合中的對象引用在使用完後未清空,使得 JVM 不能回收;
代碼中存在死循環或循環產生過多重複的對象;
啓動參數中內存的設定值過小;
OutOfMemoryError 的解決辦法需要視情況而定,但問題的根源在於程序的設計不夠合理,需要通過一些性能檢測才能找得出引發問題的根源。

導致 NoClassDefFoundError 的原因只有一個,Java 虛擬機在編譯時能找到類,而在運行時卻找不到。
在這裏插入圖片描述
NoClassDefFoundError 的解決辦法,我截了一張圖,如上所示。當一個項目引用了另外一個項目時,切記這一步!

Exception(例外)通常可分爲兩類,一類是寫代碼的人造成的,比如訪問空指針(NullPointerException)。應當在敲代碼的時候進行檢查,以杜絕這類異常的發生。

if (str == null || “”.eqauls(str)) {

}

另外一類異常不是寫代碼的人造成的,要麼需要拋出,要麼需要捕獲,比如說常見的 IOException。

拋出的示例。

public static void main(String[] args) throws IOException {

InputStream is = new FileInputStream(“沉默王二.txt”);

int b;

while ((b = is.read()) != -1) {

}

}

捕獲的示例。

public static void main(String[] args) {

try {

InputStream is = new FileInputStream(“沉默王二.txt”);

int b;

while((b = is.read()) != -1) {

}

} catch (IOException e) {

e.printStackTrace();

}

}

03、

當拋出異常的時候,剩餘的代碼就會終止執行,這時候一些資源就需要主動回收。Java 的解決方案就是 finally 子句——不管異常有沒有被捕獲,finally 子句裏的代碼都會執行。

在下面的示例當中,輸入流將會被關閉,以釋放資源。

public static void main(String[] args) {

InputStream is = null;

try {

is = new FileInputStream(“沉默王二.txt”);

int b;

while ((b = is.read()) != -1) {}

} catch (IOException e) {

e.printStackTrace();

} finally {

is.close();

}

}

但我總覺得這樣的設計有點問題,因爲 close() 方法同樣會拋出 IOException:

public void close() throws IOException {}

也就是說,調用 close() 的 main 方法要麼需要拋出 IOException,要麼需要在 finally 子句裏重新捕獲 IOException。

選擇前一種就會讓 try catch 略顯尷尬,就像下面這樣。

public static void main(String[] args) throws IOException {

InputStream is = null;

try {

is = new FileInputStream(“沉默王二.txt”);

int b;

while ((b = is.read()) != -1) {}

} catch (IOException e) {

e.printStackTrace();

} finally {

is.close();

}

}

選擇後一種會讓代碼看起來很臃腫,就像下面這樣。

public static void main(String[] args) {

InputStream is = null;

try {

is = new FileInputStream(“沉默王二.txt”);

int b;

while ((b = is.read()) != -1) {}

} catch (IOException e) {

e.printStackTrace();

} finally {

try {

is.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

總之,我們需要另外一種更優雅的解決方案。JDK7 新增了 Try-With-Resource 語法:如果一個類(比如 InputStream)實現了 AutoCloseable 接口,那麼就可以將該類的對象創建在 try 關鍵字後面的括號中,當 try-catch 代碼塊執行完畢後,Java 會確保該對象的 close方法被調用。示例如下。

public static void main(String[] args) {

try (InputStream is = new FileInputStream(“沉默王二.txt”)) {

int b;

while ((b = is.read()) != -1) {

}

} catch (IOException e) {

e.printStackTrace();

}

}

04、

關於異常處理機制的使用,我這裏總結了一些非常實用的建議,希望你能夠採納。

1)儘量捕獲原始的異常。

實際應該捕獲 FileNotFoundException,卻捕獲了泛化的 Exception。示例如下。

InputStream is = null;

try {

is = new FileInputStream(“沉默王二.txt”);

} catch (Exception e) {

e.printStackTrace();

}

這樣做的壞處顯而易見:假如你喊“王二”,那麼我就敢答應;假如你喊“老王”,那麼我還真不敢答應,萬一你喊的我妹妹“王三”呢?

很多初學者誤以爲捕獲泛化的 Exception 更省事,但也更容易讓人“丈二和尚摸不着頭腦”。相反,捕獲原始的異常能夠讓協作者更輕鬆地辨識異常類型,更容易找出問題的根源。

2)儘量不要打印堆棧後再拋出異常

當異常發生時打印它,然後重新拋出它,以便調用者能夠適當地處理它。就像下面這段代碼一樣。

public static void main(String[] args) throws IOException {

try (InputStream is = new FileInputStream(“沉默王二.txt”)) {

}catch (IOException e) {

e.printStackTrace();

throw e;

}

}

這似乎考慮得很周全,但是這樣做的壞處是調用者可能也打印了異常,重複的打印信息會增添排查問題的難度。

java.io.FileNotFoundException: 沉默王二.txt (系統找不到指定的文件。)

at java.io.FileInputStream.open0(Native Method)

at java.io.FileInputStream.open(FileInputStream.java:195)

at java.io.FileInputStream.(FileInputStream.java:138)

at java.io.FileInputStream.(FileInputStream.java:93)

at learning.Test.main(Test.java:10)

Exception in thread “main” java.io.FileNotFoundException: 沉默王二.txt (系統找不到指定的文件。)

at java.io.FileInputStream.open0(Native Method)

at java.io.FileInputStream.open(FileInputStream.java:195)

at java.io.FileInputStream.(FileInputStream.java:138)

at java.io.FileInputStream.(FileInputStream.java:93)

at learning.Test.main(Test.java:10)

3)千萬不要用異常處理機制代替判斷

我曾見過類似下面這樣奇葩的代碼,本來應該判 null 的,結果使用了異常處理機制來代替。

public static void main(String[] args) {

try {

String str = null;

String[] strs = str.split(",");

} catch (NullPointerException e) {

e.printStackTrace();

}

}

捕獲異常相對判斷花費的時間要多得多!我們可以模擬兩個代碼片段來對比一下。

代碼片段 A:

long a = System.currentTimeMillis();

for (int i = 0; i < 100000; i++) {

try {

String str = null;

String[] strs = str.split(",");

} catch (NullPointerException e) {

}

}

long b = System.currentTimeMillis();

System.out.println(b - a);

代碼片段 B:

long a = System.currentTimeMillis();

for (int i = 0; i < 100000; i++) {

String str = null;

if (str != null) {

String[] strs = str.split(",");

}

}

long b = System.currentTimeMillis();

System.out.println(b - a);

100000 萬次的循環,代碼片段 A(異常處理機制)執行的時間大概需要 1983 毫秒;代碼片段 B(正常判斷)執行的時間大概只需要 1 毫秒。這樣的比較雖然不夠精確,但足以說明問題。

4)不要盲目地過早捕獲異常

如果盲目地過早捕獲異常的話,通常會導致更嚴重的錯誤和其他異常。請看下面的例子。

InputStream is = null;

try {

is = new FileInputStream(“沉默王二.txt”);

} catch (FileNotFoundException e) {

e.printStackTrace();

}

int b;

try {

while ((b = is.read()) != -1) {

}

} catch (IOException e) {

e.printStackTrace();

}

finally {

try {

is.close();

} catch (IOException e) {

e.printStackTrace();

}

}

假如文件沒有找到的話,InputStream 的對象引用 is 就爲 null,新的 NullPointerException 就會出現。

java.io.FileNotFoundException: 沉默王二.txt (系統找不到指定的文件。)

at java.io.FileInputStream.open0(Native Method)

at java.io.FileInputStream.open(FileInputStream.java:195)

at java.io.FileInputStream.(FileInputStream.java:138)

at java.io.FileInputStream.(FileInputStream.java:93)

at learning.Test.main(Test.java:12)

Exception in thread “main” java.lang.NullPointerException

at learning.Test.main(Test.java:28)

NullPointerException 並不是程序出現問題的本因,但實際上它出現了,無形當中干擾了我們的視線。正確的做法是延遲捕獲異常,讓程序在第一個異常捕獲後就終止執行。

05、

好了,關於異常我們就說到這。異常處理是程序開發中必不可少的操作之一,但如何正確優雅地對異常進行處理卻是一門學問,好的異常處理機制可以確保程序的健壯性,提高系統的可用率。

喜歡的點點關注,點點贊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章