Java的异常
文章目录
一、只针对异常的情况才使用异常
异常应该只用于异常的情况下:它们永远不应该用于正常的控制流。
下面的代码是**错误**用法:
/**
* 异常的错误用法
* @param range
*/
private static void loopByExceptionTest(String[] range){
try{
int i=0;
while (true){
System.out.println(range[i++]);
}
}catch (ArrayIndexOutOfBoundsException e){
}
}
基于异常的循环模式不仅模糊了代码的意图,降低了它的性能,而且它还不能保证正确工作!
一般地,应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法。
设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。
二、对可恢复的情况使用受检异常,对于编程错误使用运行时异常
Java程序设计语言提供了三种可抛结构(throwable):受检异常(checked exception)、运行时异常(run-time exception)、错误(error)。
2.1 使用受检异常还是未受检异常
使用受检异常还是未受检异常时,主要原则是:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常。
通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。
因此,方法中声明要抛出的每个受检异常,都是对API用户对一种潜在指示:与异常相关联的条件时调用这个方法的一种可能的结果。
2.2 有两种未受检的可抛出结构:运行时异常和错误
在行为上两者是等同的:它们都是不需要也不应该被捕获的可抛出结构。
如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益。
如果程序没有捕捉到这样的可抛结构,将会导致当前线程中断,并出现适当的错误消息。
2.3 运行时异常
用运行异常来表示编程错误。大多数的运行时异常都表示前提违例。
所谓前提违例是指API的客户没有遵守API规范建立的约定。例如,违反了数组下标值的约定,就会出现
ArrayIndexOutOfBoundsException异常。
对于要处理可恢复的条件,还是处理编程错误,情况并非总是那么黑白分明。
如果你相信一种情况可能允许恢复,就使用受检异常;如果不是,则使用运行时异常。如果不清楚是否有可能恢复,最好使用未受检的异常。
2.4 错误
**错误**往往被JVM保留下来使用,以表明资源不足、约束失败,或者其他使程序无法继续执行的条件。
由于这已经是个几乎被普遍接受的惯例,因此最好不要再实现任何新的Error子类。
你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接或间接)。不仅不应该定义Error子类,甚至也不应该抛出AssertionError异常。
2.5 为异常提供方法
异常也是个完全意义上的对象,可以在它上面定义任意的方法。
这些方法的主要用途是为了捕获异常的代码而提供额外的信息,特别是关于引发这个异常条件的信息。
对于受检异常往往指明了可恢复的条件。所以,对于这样的异常,提供一些辅助方法尤其重要,通过这些方法,调用者可以获得一些有助于恢复的信息。
三、避免不必要地使用受检异常
受检异常,它们强迫程序员处理异常的条件,大大增强了可靠性。
如果方法抛出受检异常,调用该方法的代码就必须在一个或多个catch块中处理这些异常,或者它必须声明抛出受检异常,调用该方法的代码就必须在一个或者多个catch快中处理这些异常,或者它必须声明抛出这些异常,并让它们传播出去。
无论哪种处理受检异常的方法,都给程序员增添了不可忽视的负担。这种负担在Java8中更重了,因为抛出受检异常的方法不能直接在Stream中使用。
石蕊测试:是指简单而具有决定性的测试。
3.1 程序员如何处理受检异常
方案一:
try {
...
}catch (TheCheckedException e){
throw new AssertionError();// Can't happen!
}
方案二:
try {
}catch (TheCheckedException e){
e.printStackTrace(); // Oh well, we lose
System.exit(1);
}
如果使用API的程序员无法做得比这更好,那么未受检的异常可能更为合适。
3.2 消除受检异常的方法
(1)返回所要的结果类型的一个optional
这是最简单的一种方法。这种方法不抛出受检异常,而只是返回一个零长度的optional。
这种方法的缺点是,方法无法返回任何额外的信息,来详细说明它无法执行你想要的计算。
相反,异常则具有描述性的类型,并且能够导出方法,以提供额外的信息。
(2)把抛出异常的方法分成两个方法,其中第一个方法返回一个boolean值,表明是否应改抛出异常
把下面的调用序列:
try {
obj.action(args);
}catch (TheCheckedException e){
... // Handle exceptional condition
}
重构成:
if (obj.actionPermitted(args)){
obj.action(args);
}else {
... // handle exception condition
}
四、优先使用标准的异常
代码重用是值得提倡的,这是一条通用的规则,异常也不例外。
专家追求并且通常也能勾实现高度的代码重用。
Java平台类库提供了一组基本的未受检异常,它们满足了绝大多数API的异常抛出需求。
重用标准异常的好处:
- 使API更易于学习和使用;
- 对于使用这些API的程序而言,它们的可读性更好;
- 异常越少,意味着内存占用就越小,装载这些类的时间开销就越少;
4.1 经常被重用的异常
(1)IllegalArgumentException
当调用传递的参数值不合适的时候,往往就会抛出这个异常。
(2)IllegalStateException
如果因为接收对象的状态而调用非法,通常抛出这个异常。
例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
也可以这么说,所有错误的方法调用都可以被归结为非法参数或者非法状态。
(3)NullPointerException
如果调用者在某个不允许null值的参数中传递了null。
(4)IndexOutOfBoundsException
如果调用者在表示序列下表的参数中传递了越界的值。
(5)ConcurrentModificationException
如果监测到一个专门设计用于单线程的对象,或者与外部同步机制配合使用的对象正在(或已经)被并发修改,就应该抛出这个异常。
这个异常顶多就是一个提示,因为不可能可靠地侦测到并发到修改。
(6)UnsupportedOperationException
如果对象不支持请求的操作,就会抛出这个异常。
很少使用,应为绝大多数对象都会支持他们实现的所有方法。
如果类没有实现由它们实现的接口所定义的一个或者多个可选操作,它就可以使用这个异常。
例如,对于只支持追加操作的List实现,如果有人试图从列表中删除元素,它就会抛出这个异常。
**不要直接重用Exception、RuntimeException、Throwable、Error。**对待这些类要像对待抽象类一样。
你无法可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常的超类。
4.2 其他可被重用的异常
在条件许可的情况下,其他的异常也可以被重用。
ArithmeticException和NumberFormatException
这种重用必须建立在语义的基础上,而不是建立在名称的基础之上。
4.3 异常是可以序列化的
异常是可序列化的,这正是“如果没有非常正当的理由,千万不要自己编写异常类的原因。
五、抛出与抽象对应的异常
5.1 异常转译
更高层的实现应该捕捉低层的异常,同时抛出可以按照高层抽象进行解释的异常。
try {
}catch (LowerLevelException e){
throw new HigherLevelException(...);
}
例如:AbstractSequentialList
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
5.2 异常链
异常链是一种特殊的异常转译形式。
如果底层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很适合。
底层的异常被传到高层的异常,高层的异常提供访问方法(Throwable的getCause方法)来获得底层的异常;
try{
....
}catch (LowerLevelException cause){
throw new HigherLevelException(cause);
}
高层异常的构造器将原因传到支持链的超级构造器,因此它最终将被传到Throwable的其中一个运行异常链的构造器,例如Throwable(Throwable):
// Exception with chaining-aware constructor
class HigherLevelException extends Exception{
HigherLevelException(Throwable cause){
super(cause);
}
}
对于没有支持链的异常,可以利用Throwable的initCause方法设置原因。
尽管异常转译与不加选择地从低层传递异常的做法相比所有改进,但是也不能滥用它。
最好的最发是,在调用底层方法之前确保它们会成功执行,从而避免它们抛出异常。
六、每个方法抛出的所有异常都要建立文档
仔细为每个方法抛出的异常建立文档是特别重要的。
6.1 始终要单独地声明受检异常
利用Javadoc的@throws标签,准确记录下抛出每个异常的条件。
永远不要声明一个公有方法直接throws Exception,或者更糟糕的是声明它直接throws Throwable。
这样的声明不仅没有为程序员提供关于"这个方法能够抛出哪些异常"的任何知道信息,而且大大地妨碍了该方法的使用。
有一个例外,就是main方法,它可以安全地声明抛出Exception,因为它只通过虚拟机调用。
6.2 如同受检异常一样,仔细地为未受检异常建立文档是非常明智的
未受检异常通常代表编程上的错误。
让程序员了解所有这些错误都有助于帮助他们避免同样的错误。
对于接口的方法,在文档中记录下它可能抛出的未受检异常显得尤为重要。
使用Javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。
6.3 在类的文档注释上对(特殊)异常建立文档
如果一个类的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,而不是单独建立文档。
七、在细节信息中包含失败——捕获信息
异常类型的toString方法应该尽可能多地返回有关失败原因的信息。
-
为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和域的值
例如数组的角标
-
千万不要在细节消息中包含密码、密钥以及类似的信息
-
一种办法时在异常的构造器而不是字符串细节信息中引入这些信息
// IndexOutOfBoundsException public IndexOutOfBoundsException(int index) { super("Index out of range: " + index); }
八、努力使失败保持原子性
一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。
(1)设计一个不可能的对象
(2)在执行操作之前检查参数的有效性;
// Stack
public Object pop(){
if (size==0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
(3)调整计算机处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生
(4)在对象的一份拷贝上执行操作
(5)编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前到状态
这种方法主要用于永久性的(基于磁盘的)数据结构。
(6)对于某些操作,保持原子性会显著增加开销或复杂度
当方法抛出
AssertionError时,不需要努力保持失败原子性。
九、不要忽略异常
空的catch块会使异常达不到应有的目的。
忽略异常就如同忽略火警信号一样——如果八火警信号器关掉了,当真正由火灾发生时,就没有人能看到火警信号了。
如果选择忽略异常,catch块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为ignored。
本条目的建议同样适用于受检异常和未受检异常。
4846

被折叠的 条评论
为什么被折叠?



