有這樣一句話:衡量Java設計師水平和開發團隊紀律性的一個好方法,就是讀讀他們應用程序裏的異常處理代碼
異常處理雖然不是什麼高難度的技術點,但要是想要整個工程,所有的異常都考慮得周到,又處理得到,還要儘量少的使用try-catch,其實是很考驗一個人的設計能力的
至少那些只求完成工作任務,平時不鑽研技術細節的人,短時間內是很難做到這一點的。他們一般會做的,大概就是到處寫try-catch,然後對異常的處理方式都是e.printStackTrace。偶爾被業務逼得無可奈何,纔想起來加一個錯誤提示
什麼是Java的強制異常處理
Java語言在設計時,爲了保證安全性,考慮到了常見的可能發生的異常,會強制用戶對這些異常進行處理
不過在後來的實踐之中,逐漸證明這是一個比較失敗的設計,包括Java之父自己也承認了這一點
因爲很多時候我們是可以百分百確定某些異常不會發生的,或者說發生了也沒關係,完全不需要進行處理
強制異常處理屬於設計過度,強制開發者編寫try-catch,最後讓代碼變得異常繁瑣
強制異常處理案例
new Thread(new Runnable() {
@Override
public void run() {
try {
new File("C://x.mp3").createNewFile();
new FileOutputStream("C://x.mp3");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
比如我們這裏創建線程的代碼,第一行代碼創建了文件,第二行代碼獲取文件流
如果代碼執行到了第二行代碼,說明文件肯定存在,是完全不用處理FileNotFoundException的
很多時候我們會被強制異常處理機制逼着編寫這樣冗餘且繁瑣的代碼,讓整個代碼變得很難看
巧妙迴避Java的強制異常處理機制
雖然Java SDK的已有代碼,但我們可以將這些異常處理邏輯封裝起來,對異常進行統一處理,這樣我們在開發業務代碼時,就可以大幅減少try-catch代碼了
這裏還是以上面的線程爲例,來展示下如何通過設計技巧,迴避Java的強制異常處理機制
我們先創建一個Action接口來取代Runnable接口,因爲Runnable是Java內置代碼,不可能再修改了
//封裝一組行爲,和Runnable功能是一樣的,但是迴避了Java的強制處理機制
//由於run方法增加了throws Exception選型,將異常拋給了調用者處理,開發者在實現run方法時就不用處理強制異常了
//雖然run方法不用處理異常,但是runIgnoreException調用了run方法,等於接收了run方法的強制異常,需要對它們進行處理
//我們進一步在runIgnoreException中將強制型異常轉化爲運行時異常拋出,運行時異常是不需要強制處理的
//當我們使用Action對象時,調用runIgnoreException方法來替代run方法,這樣就可以迴避Java的強制處理機制了
//上面的做法只是直接跳過了Java的強制異常處理機制,但並不是什麼時候我們都可以這樣做
//畢竟我們的程序不是完美的,有些異常可能是我們沒有考慮到的,一個完善的系統是不能僅僅忽略異常的
//runAndPostException提供了一個額外的功能,它對任意異常進行捕捉,然後統一轉交給Application處理
@SuppressWarnings("all")
public interface Action {
//封裝一組行爲
void run() throws Exception;
//忽略異常
default void runIgnoreException() {
try {
run();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
//將異常轉交給Application統一處理
default void runAndPostException() {
try {
run();
} catch (Throwable e) {
CommonApplication.ctx.handleGlobalException(e);
}
}
}
下面我們再封裝一個方法,用Action代替Runnable來完成線程工作
public class Threads {
public static void post(Action action) {
new Thread(action::runAndPostException).start();
}
}
Threads + Action取代Thread + Runnable完成線程工作
Threads.post(()->{
new File("C://x.mp3").createNewFile();
new FileOutputStream("C://x.mp3");
});
可以看到,終於不用再進行強制異常處理了,而且代碼還變得更加簡潔了
Action不但幫我們迴避了強制異常處理機制,還可以把所有異常交給Application處理
這樣我們就可以在Application裏面對所有異常進行統一處理,其它地方都不用編寫try-catch代碼了,以更少代碼完成更強功能!
以上設計方法和UncaughtExceptionHandler的區別
有些人可能知道,通過Thread.setDefaultUncaughtExceptionHandler也可以對線程進行統一異常捕捉,但是它的功能很有限
首先,UncaughtExceptionHandler只是捕獲未處理的異常,它並不能迴避強制異常處理機制
其次,UncaughtExceptionHandler是對整個線程進行異常捕捉的,相當於在線程run方法的開頭和結尾加了一個默認的try-catch
當線程發生異常時,代碼就會跳到最後的UncaughtExceptionHandler處理中,雖然異常被捕捉了,但整個線程也停止工作了
而我們這套設計方法,不但可以迴避強制異常處理機制,還能保證線程的繼續運行
上面只是一個簡單Demo,看不出二者差距,下面舉兩個例子來說明區別
案例一
Action action;
Threads.post(()->{
while (true)
action.runAndPostException();
});
Action的異常處理範圍是run方法,當run方法發生異常時,只是當前run方法結束,while還會繼續跳到下一個run方法執行
而UncaughtExceptionHandler的異常處理範圍是整個線程,一旦發生異常,整個線程都結束了
案例二
Button button;
Action listener;
button.setOnClickListener(()->{
listener.runAndPostException();
});
這裏我們給按鈕添加了一個點擊事件,當run方法發生異常時,只是當前run方法結束,並不影響整個線程
如果使用UncaughtExceptionHandler的話,意味着整個線程都要結束了
有過界面開發經驗的朋友應該知道,一般控件事件都是運行在主線程的,主線程結束了,也就意味着整個界面要崩潰了
關聯
其實上面兩個案例是有關聯的在界面應用開發中,一般都是將用戶的點擊事件存儲到事件隊列中,監聽器作爲某個事件的回調,主線程會輪詢這個事件隊列,逐個取出事件的回調對象來執行
按照我們的設計方法,即便一個事件回調發生異常,執行失敗了,隊列還會繼續輪詢其它的事件,不影響整個事件隊列
下面用僞代碼來簡單模擬下事件隊列的工作機制
Queue<Event> eventQueue;
while (true){
if(eventQueue.isEmpty()) continue;
Event event = eventQueue.poll();
Action listener = event.listener;
listener.runAndPostException();
}
總結
UncaughtExceptionHandler只是對沒catch到的代碼進行捕捉下,我們最多紀錄下異常信息,並不能改變線程本身的運行流程
而我們的設計方法,則可以靈活變通,既能用於整個線程的異常處理,也能用於單個方法的異常處理
我們可以封裝一個Action接口,同樣可以將這套方法應用於其它的類或接口
Action提供了三個接口方法,它們的處理方法分別是:
- run:將異常交給調用者處理
- runIgnoreException:忽略異常處理
- runAndPostException:將異常轉交給Application統一處理
具體使用哪個,大家可以根據實際需要來決定