这是我参与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
禁止抓取堆栈来提升异常的效率。
这带来的缺点就是没有了堆栈,没法定位异常。可是,并非全部的异常咱们都须要其堆栈信息的,对于咱们已知的异常,例如参数校验所抛的异常就没有必要记录堆栈,这时咱们就能够优化异常的效率。
可是,对于咱们不可预见的,未知的系统异常,保留堆栈是很是有必要的!!!