您的位置:首页 > 编程语言 > Java开发

Java 异常的使用清单 —— 原来这才是异常的正确打开方式

2017-07-02 19:13 639 查看
本文结合《Effective Java》第九章《异常》和自己的理解及实践,讲解了正确使用Java异常的优秀指导原则,文章发布于专栏Effective Java,欢迎读者订阅。

充分发挥异常的优点,可以提高程序的可读性、可靠性和可维护性,如果使用不当,则会带来负面影响,下面几条清单,希望能帮助您正确使用Java异常。

清单1: 只针对异常的情况才使用异常

曾经有个哥们把异常当做流程控制的工具,写了这么一段代码:

try {
int[] arr = {1,2,3,4,5};
int i = 0;
while(true){
System.out.println(arr[i++]);
}
} catch (ArrayIndexOutOfBoundsException e) {
}


而程序员都知道,标准的循环模式应该是这样的:

for(int j : arr){
System.out.println(j);
}

写出第一种写法的程序员,往往秉持着这样子的观点:标准的循环模式每次都会去检查角标是否越界,效率更慢。

对此,我们可以做一个小实验,测试一下两种方式的速度:

private static void test1() {
int size = 100000;
int[] arr = new int[size];
for(int i=0;i<size;i++){
arr[i] = i;
}
long beginTime = System.currentTimeMillis();

// use try-catch cycle
try {
int i = 0;
while(true){
System.out.println(arr[i++]);
}
} catch (ArrayIndexOutOfBoundsException e) {
}

// use for-each cycle
//		for(int j : arr){
//			System.out.println(j);
//		}

long endTime = System.currentTimeMillis();
System.out.println(endTime-beginTime);
}

在我的机器上,十万个元素的数组,for循环每次都比try-catch要快几十毫秒。
对此,《Effective Java》给出的解释是:

异常机制设计初衷是用于不正常的情形,很少有JVM会对异常进行优化,也就是说,把代码放在try-catch块中,会阻止JVM对代码的优化;

标准模式下,并不会导致冗余的检查,很多JVM都会对这种代码进行优化。

而且基于异常的循环,还会掩盖掉代码中的bug,假如代码中确实访问了越界的元素,那么正常流程会抛出异常,打印堆栈,提供定位的线索,而基于异常的循环,则会将这个bug悄悄的掩盖掉。

因此,异常只应该用于异常的情况,不应该用于控制流。

这个例子也启发我们,设计良好的API,不应该强迫调用者使用异常来进行正常的控制流程。比如Iterator接口,提供了一个hasNext方法,来让我们在循环之前判断能不能继续遍历,而不是等到遍历时抛异常再来进行循环的终止。

清单2: 对可恢复的情况使用受检异常,对编程错误使用运行时异常

所有的异常都是throwable的,throwable下面有三种结构:受检异常、运行时异常和错误。

如果期望调用者在遇到异常后可以进行恢复的操作,那么使用受检异常,也就是 xxx extends Exception的异常。例子:数据库断连,抛出受检异常,让调用者再去尝试连接。

如果不希望调用者进行恢复,想直接中断程序的运行,则抛出运行时异常,也就是 xxx extends RuntimeException的异常。例子:数组越界异常,属于代码中没有做好充足的校验,访问了越界的元素。

对于错误,我们不需要自己去实现Error的子类,Jdk里面已经定义了所有的错误,当我们需要自定义未受检异常时,应该去继承RuntimeException,而不是Error.

清单3: 避免不必要的使用受检异常

如果一个API抛出了受检异常,那么就要求调用者在它的代码中使用try-catch捕获或者使用throws向上抛,这都会给调用者带来麻烦。

有一个办法可以避免不必要的受检异常,那就是清单1提到过的状态检查方法,比如对于下面这段try-catch代码:

try {
obj.action(args);
} catch(CheckedException e) {
....
}

我们可以在调用action方法前,提供一个辅助的校验方法:

if(obj.actionPermmited(args)) {
obj.action(args);
} else {
}


当然,如果是以下两种情况,那么这种方法就不适用:
对象在缺陷外部同步的情况下被并发访问,那么在actionPermmited和action之间,对象状态可能被改变,不再满足校验,那么action方法同样会执行失败。
actionPermmited方法必须重复action方法的操作,那么从性能上考虑,这种重构就不值得。

清单4: 优先使用标准异常

Java平台提供了一组基本的未受检异常,它们满足了绝大多数API的异常抛出需要,所以在创建自己的异常时,先考虑一下现有的异常是否满足需要。

下面是这些常用的异常列表:

IllegalArgumentException                    非null的参数值不满足API需要
IllegalStateException                           对象状态不合适
NullPointerException                           在禁止使用null的情况下使用了null的参数
IndexOutOfBoundsException              下标越界
ConcurrentModificationException        在禁止并发修改的情况下,检测到对象的并发修改
UnsupportedOperationException         不支持的操作方法

比如,我们开发了一个纸牌游戏,客户端传入一个值为99的纸牌,不在我们1~15的范围(姑且认为 14小王 15大王),我们可以抛出IllegalArgumentException;再比如,客户端传来一只K和一只大王的组合牌,那么我们可以抛出UnsupportedOperationException。

清单5: 抛出与抽象相对应的异常

如果方法抛出的异常与它所执行的任务没有明显的联系,那么这种异常会让调用者不知所措,这时候就需要进行异常转译,以JDK的AbstractSequentialList类为例:

/**
* Returns the element at the specified position in this list.
*
* <p>This implementation first gets a list iterator pointing to the
* indexed element (with <tt>listIterator(index)</tt>).  Then, it gets
* the element using <tt>ListIterator.next</tt> and returns it.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}


get方法是通过一个角标去获取对象,如果不进行异常转译,直接抛出NoSuchElementException,显然不合适,因此在捕获了NoSuchElementException之后,进行了异常的转译,抛出IndexOutOfBoundsException。

清单6: 方法抛出的每个异常都要有文档说明

Java语言要求程序员为每个方法声明它可能抛出的受检异常,但是对于未受检的异常,却没有这个要求。

然而,同受检异常一样,给每个可能的未受检异常创建Javadoc文档说明,也是很有必要的,可以帮助调用者避免犯错误。

异常的文档格式很简单,在专栏之前的一篇文章中提到过:Java 设计方法的五条优秀实践清单 —— 为每个方法编写Javadoc文档注释
大致的格式就是 @throws + 异常类型 + 什么情况下会抛出此异常

/**
* Returns the element at the specified position in this list.
*
This method is thread-safe, and it will start a thread.
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
*         (<tt>index < 0 || index >= size()</tt>)
*/


清单7: 在异常的消息中包含能定位失败原因的信息

这一点,在项目后期维护中尤为重要,很多问题并非每次都能重现,也许运行一百万次才能重现一次,这时候,如果异常的信息不能充分到能够帮助程序员分析出问题的原因,那就麻烦大了。

为了能够定位出失败的原因,异常的消息中应该包括所有对造成该异常有关的参数和域的值。比如对于IndexOutOfBoundsException,必要的信息就包括:下界、上界和导致异常的下标值。

清单8: 努力使失败保持原子性

对于受检异常,我们都希望对象保持在调用前的状态,具有这种属性的方法,我们称之为失败原子性。

实现失败原子性的方法有以下几种:

设计一个不可变对象(参考  Java 设计类和接口的八条优秀实践清单  —— 清单3 使类的可变性最小化),如果对象是不可变的,那么失败原子性就是必然的了。
执行状态修改操作前进行有效性检查,如果检查失败,就不进行状态的修改。
调整处理过程的顺序,把有可能失败的操作放在前面,把会修改对象状态的操作放在后面。
写一段恢复代码,在失败后将对象回滚到之前的状态。
在对象的临时拷贝上进行操作,操作成功再把对象指向拷贝对象。

清单9: 不要忽略异常

我们经常会见到这样的代码,在catch代码块里没有做任何操作:

try {

} catch (SomeException e) {

    //do nothing

}

我的建议是,即使不需要做恢复动作,那么也应该将错误打印的日志中。

以上,希望能对您正确的使用Java异常有所帮助。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java 异常
相关文章推荐