這是我參與8月更文挑戰的第7天,活動詳情查看:8月更文挑戰java
在Java語言中,正如Object是全部對象的父類同樣,Throwable是全部異常的父類。爲何會有異常類呢?程序是人開發出來的,而人不免是會犯錯誤的,所以程序可能會運行異常。一旦發生了異常,開發者首先要作的就是定位異常,而後解決異常。git
如何解決異常那是開發者要作的事情,如何讓開發者快速定位到異常,倒是Java語言自己的職責。 shell
所以,異常的基類Throwable有一個很是重要的屬性【stackTrace】,它表明出現異常時,當前線程運行的堆棧信息。經過它,能夠快速定位到該異常是在哪一個類的哪一個方法的第幾行代碼被拋出的。markdown
// 異常詳細信息
private String detailMessage;
// 堆棧列表
private StackTraceElement[] stackTrace;
複製代碼
其中,detailMessage是開發者手動指定的,而stackTrace堆棧則由JVM自動抓取。 ide
以下示例程序,無條件拋異常。函數
public class Demo {
public static void main(String[] args) {
throwException();
}
static void throwException() {
throw new RuntimeException("拋異常了...");
}
}
複製代碼
控制檯輸出以下:post
Exception in thread "main" java.lang.RuntimeException: 拋異常了...
at top.javap.exception.Demo.throwException(Demo.java:10)
at top.javap.exception.Demo.main(Demo.java:6)
複製代碼
經過控制檯輸出的異常信息就能夠快速定位到異常,很是的方便。此時,你確定會感嘆,JVM抓取的堆棧信息竟是如此的好用,一眼即可定位到異常。 性能
好用是好用,可是好用的背後是有代價的。這種異常建立的成本很是高,每個異常對象被建立時,JVM都須要抓取當前線程運行的堆棧信息。測試
異常很好用,可是切莫濫用。咱們經過一個例子來感覺一下,使用異常來處理業務邏輯到底有多慢。 優化
【需求】 給定一個字符串S,判斷S是不是數字。
【實現A-非異常】
public static boolean isNumber(String s){
for (char c : s.toCharArray()) {
// 比較每一個字符是不是數字
if (!Character.isDigit(c)) {
return false;
}
}
return true;
}
複製代碼
【實現B-異常】
public static boolean isNumber(String s) {
try {
// 嘗試強轉成Integer,轉換失敗會拋NumberFormatException
Integer.parseInt(s);
} catch (Exception e) {
return false;
}
return true;
}
複製代碼
執行一千萬次,測試結果以下:
字符串S | 方案A耗時(ms) | 方案B耗時(ms) |
---|---|---|
123 | 154 | 19 |
123a | 161 | 9809 |
能夠看到,當字符串S爲正常數字時,方案B不會拋異常,二者的性能差很少,甚至方案B還會更好一下。 一旦字符串S爲非數字時,方案B開始拋異常,性能直線降低,比非異常的方式慢了近60倍!!!
這個結果已經很直觀了,足夠說明問題了吧。
異常很好用,在業務處理中,若是判斷操做不符合要求,直接拋一個異常,結束流程的執行,很方便。可是慢慢的,整個系統就會出現【異常濫用】的狀況。
魚和熊掌如何兼得? 我既想要異常的方便,又不想由於它而影響性能,有什麼好的辦法嗎?固然有,那就是下降異常建立的成本。
異常對象建立的成本之因此高,主要就是由於它在構造函數中調用了fillInStackTrace()
方法抓取了堆棧信息,這個過程開銷極大。
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null fillInStackTrace(0);
stackTrace = UNASSIGNED_STACK;
}
return this;
}
複製代碼
只要跳過這個步驟,建立異常對象就跟建立普通對象沒什麼兩樣了。
fillInStackTrace()
方法並無被final
修飾,這意味着子類能夠重寫該方法,所以咱們只須要建立一個輕量級的業務異常類,重寫該方法便可實現高效異常類。
public class LightBizException extends BizException {
public LightBizException(String message) {
super(message);
}
@Override
public synchronized Throwable fillInStackTrace() {
// 重寫,禁止抓取堆棧信息
return this;
}
}
複製代碼
還有另外一種方式,在構造函數中將writableStackTrace
置爲false便可,這樣也不會抓取堆棧信息。
/** * @param message 異常詳細信息 * @param cause 當前異常由哪一個異常引發 * @param enableSuppression 是否啓用抑制異常 * @param writableStackTrace 是否啓用堆棧跟蹤 */
protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
if (writableStackTrace) {
fillInStackTrace();// 抓取堆棧信息
} else {
stackTrace = null;
}
}
複製代碼
上述兩種方法均可以實現高效的異常類,只要不抓取堆棧信息,異常類的建立成本會大大下降。這樣,既能夠方便的使用異常作流程控制,又不用擔憂性能問題,魚和熊掌兼而有之。
不抓取堆棧信息的輕量級異常類也是有缺點的,那就是你再也沒法追蹤到它了。沒有了堆棧,你難以定位異常是在哪裏產生的。可是回過頭來想想,追蹤不到就追蹤不到嘛,你真的須要全部異常的堆棧嗎?
在處理業務邏輯時,不少時候作業務校驗,只是爲了過濾非法請求,拋一個異常,拒絕執行後續的業務邏輯,異常的目的僅僅是作流程控制。例如一個修改用戶信息的方法,最基本的就是校驗用戶ID不能爲空,這個異常是咱們已知的,那你以爲這種異常的堆棧還有意義嗎?
異常很好用,可是異常對象建立的成本過高了,默認每次都會抓取堆棧信息,這也是建立成本高的主要緣由之一,咱們能夠經過重寫fillInStackTrace()
方法或在構造函數中指定writableStackTrace
禁止抓取堆棧來提升異常的效率。
這帶來的缺點就是沒有了堆棧,沒法定位異常。可是,並非全部的異常咱們都須要其堆棧信息的,對於咱們已知的異常,例如參數校驗所拋的異常就沒有必要記錄堆棧,這時咱們就能夠優化異常的效率。
可是,對於咱們不可預見的,未知的系統異常,保留堆棧是很是有必要的!!!