专业的JAVA编程教程与资源

网站首页 > java教程 正文

如何优化Java异常的效率?(java如何优雅的处理异常)

temp10 2024-10-26 15:15:03 java教程 14 ℃ 0 评论

1. 前言

在Java语言中,正如Object是所有对象的父类一样,Throwable是所有异常的父类。为什么会有异常类呢?程序是人开发出来的,而人难免是会犯错误的,因此程序可能会运行异常。一旦发生了异常,开发者首先要做的就是定位异常,然后解决异常。


如何优化Java异常的效率?(java如何优雅的处理异常)

如何解决异常那是开发者要做的事情,如何让开发者快速定位到异常,却是Java语言本身的职责。


因此,异常的基类Throwable有一个非常重要的属性【stackTrace】,它代表出现异常时,当前线程运行的堆栈信息。通过它,可以快速定位到该异常是在哪个类的哪个方法的第几行代码被抛出的。

// 异常详细信息
private String detailMessage;

// 堆栈列表
private StackTraceElement[] stackTrace;

其中,detailMessage是开发者手动指定的,而stackTrace堆栈则由JVM自动抓取。


如下示例程序,无条件抛异常。

public class Demo {

	public static void main(String[] args) {
		throwException();
	}

	static void throwException() {
		throw new RuntimeException("抛异常了...");
	}
}

控制台输出如下:

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都需要抓取当前线程运行的堆栈信息。

2. 性能对比

异常很好用,但是切莫滥用。我们通过一个例子来感受一下,使用异常来处理业务逻辑到底有多慢。


【需求】

给定一个字符串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倍!!!


这个结果已经很直观了,足够说明问题了吧。


3. 优化异常效率

异常很好用,在业务处理中,如果判断操作不符合要求,直接抛一个异常,结束流程的执行,很方便。但是慢慢地,整个系统就会出现【异常滥用】的情况。


鱼和熊掌如何兼得?

我既想要异常的方便,又不想因为它而影响性能,有什么好的办法吗?当然有,那就是降低异常创建的成本。


异常对象创建的成本之所以高,主要就是因为它在构造函数中调用了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;
    }
    detailMessage = message;
    this.cause = cause;
    if (!enableSuppression)
        suppressedExceptions = null;
}

上述两种方法都可以实现高效的异常类,只要不抓取堆栈信息,异常类的创建成本会大大降低。这样,既可以方便地使用异常做流程控制,又不用担心性能问题,鱼和熊掌兼而有之。


4. 思考

不抓取堆栈信息的轻量级异常类也是有缺点的,那就是你再也无法追踪到它了。没有了堆栈,你难以定位异常是在哪里产生的。但是回过头来想一想,追踪不到就追踪不到嘛,你真的需要所有异常的堆栈吗?


在处理业务逻辑时,很多时候做业务校验,只是为了过滤非法请求,抛一个异常,拒绝执行后续的业务逻辑,异常的目的仅仅是做流程控制。例如一个修改用户信息的方法,最基本的就是校验用户ID不能为空,这个异常是我们已知的,那你觉得这种异常的堆栈还有意义吗?


5. 总结

异常很好用,但是异常对象创建的成本太高了,默认每次都会抓取堆栈信息,这也是创建成本高的主要原因之一,我们可以通过重写fillInStackTrace()方法或在构造函数中指定writableStackTrace禁止抓取堆栈来提高异常的效率。


这带来的缺点就是没有了堆栈,无法定位异常。但是,并不是所有的异常我们都需要其堆栈信息的,对于我们已知的异常,例如参数校验所抛的异常就没有必要记录堆栈,这时我们就可以优化异常的效率。


但是,对于我们不可预见的,未知的系统异常,保留堆栈是非常有必要的!!!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表