首页 > 学院 > 开发设计 > 正文

Effective Java读书笔记五:异常(57-65)

2019-11-14 09:39:09
字体:
来源:转载
供稿:网友

第57条:只针对异常的情况才使用异常

异常是为了在异常情况下使用而设计的,不要将它们用于普通的控制流,也不要编写迫使它们这么做的API。

下面部分来自:异常

如果finally块中出现了异常没有捕获或者是捕获后重新抛出,则会覆盖掉try或catch里抛出的异常,最终抛出的异常是finally块中产生的异常,而不是try或catch块里的异常,最后会丢失最原始的异常。

如果在try、catch、finally块中都抛出了异常,只是只有一个异常可被传播到外界。记住,最后被抛出的异常是唯一被调用端接受到的异常,其他异常都被掩盖而后丢失掉了。如果调用端需要知道造成失几的初始原因,程序之中就绝不能掩盖任何异常。

请不要在try块中发出对return、break或continue的调用,万一无法避免,一定要确保finally的存在不会改变函数的返回值(比如说抛异常啊、return啊以及其他任何引起程序退出的调用)。因为那样会引起流程混乱或返回值不确定,如果有返回值最好在try与finally外返回。

不要将try/catch放在循环内,那样会减慢代码的执行速度。

如果构造器调用的代码需要抛出异常,就不要在构造器处理它,而是直接在构造器声明上throws出来,这样更简洁与安全。因为如果在构造器里处理异常或将产生异常的代码放在构造器之外调用,都将会需要调用额外的方法来判断构造的对象是否有效,这样可能忘记调用这些额外的检查而不安全。

第58条:对可恢复的情况使用受检异常,对编程错误使用运用时异常

java程序设计语言提供了三种异常:受检的异常(checked exception)、运行时异常(run-time exception)和错误(error)。关于什么时候适合使用哪种异常,虽然没有明确的规定,但还是有些一般性的原则的。

检测性异常通常是由外部条件不满足而引起的,只要条件满足,程序是可以正常运行的,即可在不修改程序的前提下就可正常运行;而运行时异常则是由于系统内部或编程时人为的疏忽而引起的,这种异常一定要修正错误代码后再能正确运行。受检异常对客户是有用的,而运行时异常则是让开发人员来调试的,对客户没有多大的用处。

在决定使用受检异常还是未受检异常时,主要的原则是:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常。抛出的受检异常都是对API用户的一种潜在的指示:与异常相关的条件是调用这个方法的一种可能的结果。

有两种未受检的异常:运行时异常和错误。在行为上两种是等同:它们都不需要捕获。如果抛出的是未受检异常或错误,往往就属于不可恢复的情形,继续执行下去有害无益。如果程序未捕获这样的异常或错误,将会导致线程停止,并出现适当的错误消息。

用运行时异常来表明编程错误。大多数的运行时异常都表示违返了API规约,API的客户同有遵守API规范。例如,数组访问的约定指明了数组的下标值必须在零和数组长度减1之间,ArrayIndexOutOfBoundsException表明了这个规定。

按照惯例,错误往往被JVM保留用于表示资源不足、约束失败,或者其他程序无法继续执行的条件。由于这已经是个几乎被普遍接受的惯例,因此最好不要再实现任何新的Error子类。因此,你实现的所有未受检异常都应该是RuntimeException的子类或间接是的。

总而言这,对于可恢复的情况,使用受检的异常;对于程序错误,则使用运行时异常。当然,这也不总是这么分明的。例如,考虑资源枯竭的情形,这可能是由于程序错误而引起的,比如分配了一块不合理的过大的数组,也可能确实是由于资源不足而引起的。如果资源枯竭是由于临时的短缺,或是临时需求太大所造成的,这种情况可能就是可恢复的。API设计者需要判断这样的资源枯竭是否允许。如果你相信可允许恢复,就使用受检异常,否则使用运行时异常。如果不清楚,最好使用未受检异常。

因为受检异常往往指明了可恢复的条件,所以,这于这样的异常,提供一些辅助方法尤其重要,通过这些方法,调用都可以获得一些有助于恢复的信息。例如,假设因为没有足够的钱,他企图在一个收费电话上呼叫就会失败,于是抛出检查异常。这个异常应该提供一个访问方法,以便用户所缺的引用金额,从而可以将这个数组传递给电话用户。

第59条:避免不必要地使用受检异常

受检异常与运行时异常不一样,它们强迫程序员处理异常的条件,大大增强了可靠性,但过分使用受检异常会使用API使用起来非常不方便。如果方法抛出一个或者多个受检异常,调用都就必须在一个或多个catch块中处理,或者将它们抛出并传播出去。无论是哪种,都会给程序员添加不可忽视的负担。

如果方法只抛出单个受检异常,也会导致该方法不得在try块中,在这种情况下,应该问自己,是否有别的途径来避免API调用者使用受检的异常。这里提供这样的参考,我们可以把抛出的单个异常的方法分成两个方法,其中一个方法返回一个boolean,表明是否该抛出异常。这种API重构,把下面的调用:

try{//调用时检查异常 obj.action(args);//调用检查异常方法}catch(TheCheckedExcption e){ // 处理异常条件 ...}

重构为:

if(obj.actionPermitted(args)){//使用状态测试方法消除catch obj.action(args);}else{ // 处理异常条件 ...}

这种重构并不总是合适的,但在合适的地方,它会使用API用起来更加舒服。虽然没有前者漂亮,但更加灵活——如果程序员知道调用肯定会成功,或不介意由调用失败而导致的线程终止,则下面为理为简单的调用形式:

obj.action(args);

第60条:优先使用标准异常

常见的可重用异常:

异常 使用时机
IllegalArgumentException 非null的参数值不正确
IllegalStateException 对象状态不适合方法调用
NullPointerException 参数值是null,但这不允许
IndexOutOfBoundsException 索引参数值越界
ConcurrentModificationException 在禁止并发修改的情况下,检测到对象的并发修改。
UnsupportedOperationException 对象不支持的方法

一定要确保抛出的异常的条件与该异常的文档中的描述的条件是一致的,如果希望稍微增加更多的失败-捕获信息,可以把现有的异常进行子类化。

第61条:抛出与抽象对象相对应的异常

如果方法抛出的异常与所执行的任务没有明显的联系,这种情形将会使人不知所措,当底层的异常传播到高层时往往会出现这种情况。这了使人困惑之外,抛出的底层异常类会污染高层的API(高层要依赖于底层异常类)。为了避免这个问题,高层在捕获底层抛出的异常的同时,在捕获的地方将底层的异常转换后再重新抛出会更好:

// 异常转换try {// 调用底层方法...} catch(LowerLevelException e) { //捕获底层抛出的异常后并转换成适合自己系统的异常后再重新抛出throw new HigherLevelException(...);}

下面是个来自AbstractSequentialList类中的底层异常转换的实例,该数是List的一个抽象类,它的直接子类为LinkedList,在这个例子中,按照List接口中的get方法的规范(规范中说到:如果索引超出范围 (index < 0 || index >= size()),就会抛出IndexOutOfBoundsException异常),底层方法只要可能抛出异常,我们就需要转换这个异常,下面是AbstractSequentialList类库的做法:

/*** Returns the element at the specified position in this list.* @throws IndexOutOfBoundsException if the index is out of range* ({@code index < 0 || index >= size()}).*/public E get(int index) {ListIterator<E> i = listIterator(index);try {return i.next();//Iterator的next会抛出NoSuchElementException运行时异常} catch(NoSuchElementException e) {/** 但接口规范是要求抛出IndexOutOfBoundsException异常,所以需要转换。当然这种* 转换也是合理的,因为该方法的功能特性就是按索引来取元素,在索引越界的情况* 下抛出NoSuchElementException也是没有太大的问题的(当然劈开规范来说的),但* 抛IndexOutOfBoundsException异常会更适合一些*/throw new IndexOutOfBoundsException("Index: " + index);}}

另一种异常转换的形式是异常链,如果底层的异常对于高层调试有很大帮助时,使用异常链就非常合适,这样在高层我们可以通过相应的方法来获取底层抛出的异常:

// 异常链try {... // 调用底层方法} catch (LowerLevelException cause) { // 构造异常链后重新抛出throw new HigherLevelException(cause);}

尽管异常转换与不加选择地将捕获到的底层异常传播到高层中去相比有所改进,但是它不能滥用。处理来自底层异常的首选做法是根本就让底层抛出异常,在调用底层方法前确保它会成功,从而来避免抛出异常,另外,我们有时也可以在调用底层方法前,在高层检查一下参数的有效性,从而也可以避免异常的发生,当然这种做法(不要抛出底层异常的做法)只是对底层抛出的是运行时异常时才可行。如果确实无法避免(如低层抛出的是受检异常或是运行时异常但根本无法阻止)低层异常时,次选方案是让高层绕开这些异常,并将异常使用日志记录器记录下来供事后调试。

总之,处理底层异常最好的方法首选是阻止底层异常的发生,如果不能阻止或者处理底层异常时,一般的做法是使用异常转换(包括异常链转换),除非底层方法碰巧可以保证抛出的异常对高层也合适才可以将底层异常直接从底层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常的同时,又能捕获底层的原因进行失败分析。

第62条:每个方法抛出的异常都要有文档描述

如果一个方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这此异常类的某个超类。永远不要声明一个方法“throws Exception”,或者更糟的是声明“throws Throwable”,这是极端的例子,因为它掩盖了该方法可能抛出的其他异常。

对于方法可能抛出的未受检异常,如果将这些异常信息很好地组织成列表文档,就可以有效地描述出这个方法被成功执行的前提条件。每个方法的文档应该描述它的前提条件,这是很重要的,在文档中描述出未受检的异常是满中前提条件的最佳做法。

对于掊中的方法,在文档中描述出它可能抛出的未受检异常显得尤其重要。这份文档成了该接口的通用约定的一部分,它指定了该接口的多个实现必须遵循的公共行为。

未受检异常也要在@throws标签中进行描述。

如果某类所有方法抛出同一个异常,那么这个异常的文档可以描述在类文档中。

总之,要为你编写的每个方法所能摆好出的每个异常建立文档,对于未受检和受检异常,以及对于抽象的和具体的方法也都一样。

第63条:异常信息中要包含足够详细的异常细节消息

异常的细节消息对异常捕获者非常有用,对异常的诊断是非常有帮助的。

为了捕获失败,异常的细节消息应该包含所有“对该异常有作用”的参数和域值。例如IndexOutOfBoundsException异常的细节消息应该包含下界、上界以及没有落在界内的下标值,因为这三个值都有可能引起这个异常。

异常的细节消息不应该与“用户层次的错误消息”混为一谈,后都对于最终用户而言必须是可理解的。与用户层次的错误消息不同,异常的详细消息主要是让程序员用来分析失败原因的。因此,异常细节消息的内容比可理解性重要得多。

为了确保在异常的细节消息中包含足够的能捕获失败的信息,一种办法是在异常的构造器而不是字符串细节消息中引入这些信息。然后,有了这些信息,只要把它们放到消息描述中,就可以自动产生细节消息。例如,IndexOutOfBoundsException本应该这样设计的:

/*** Construct an IndexOutOfBoundsException.** @param lowerBound the lowest legal index value.* @param upperBound the highest legal index value plus one.* @param index the actual index value.*/public IndexOutOfBoundsException(int lowerBound, int upperBound,int index) {// 构建详细的捕获消息super("Lower bound: " + lowerBound +", Upper bound: " + upperBound +", Index: " + index);// 存储失败的细节消息供程序访问this.lowerBound = lowerBound;this.upperBound = upperBound;this.index = index;}

但遗憾的是,Java平台类库并没有使用这种做法,但是,这种做法仍然值得大力推荐。

第64条:努力使失败保持原子性

当一个对象抛出一个异常之后,我们总期望这个对象仍然保持在一种定义良好的可用状态之中。对于被检查的异常而言,这尤为重要,因为调用者通常期望从被检查的异常中恢复过来。 一般而言,一个失败的方法调用应该保持使对象保持在”它在被调用之前的状态”。具有这种属性的方法被称为具有”失败原子性(failure atomic)”。可以理解为,失败了还保持着原子性。对象保持”失败原子性”的方式有几种:

设计一个非可变对象。对于在可变对象上执行操作的方法,获得”失败原子性”的最常见方法是,在执行操作之前检查参数的有效性。如下(Stack.java中的pop方法):public Object pop() { if (size==0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; return result;}与上一种方法类似,可以对计算处理过程调整顺序,使得任何可能会失败的计算部分都发生在对象状态被修改之前。 编写一段恢复代码,由它来解释操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。在对象的一份临时拷贝上执行操作,当操作完成之后再把临时拷贝中的结果复制给原来的对象。

虽然”保持对象的失败原子性”是期望目标,但它并不总是可以做得到。例如,如果多个线程企图在没有适当的同步机制的情况下,并发的访问一个对象,那么该对象就有可能被留在不一致的状态中。

即使在可以实现”失败原子性”的场合,它也不是总被期望的。对于某些操作,它会显著的增加开销或者复杂性。 总的规则是:作为方法规范的一部分,任何一个异常都不应该改变对象调用该方法之前的状态,如果这条规则被违反,则API文档中应该清楚的指明对象将会处于什么样的状态。

第65条:不要忽略异常

当一个API的设计者声明一个方法会抛出某个异常的时候,他们正在试图说明某些事情。所以,请不要忽略它!忽略异常的代码如下:

try { ...} catch (SomeException e) {}

空的catch块会使异常达不到应有的目的,异常的目的是强迫你处理不正常的条件。忽略一个异常,就如同忽略一个火警信号一样 – 若把火警信号器关闭了,那么当真正的火灾发生时,就没有人看到火警信号了。所以,catch块至少应该包含一条说明,用来解释为什么忽略这个异常是合适的。

《Effective Java中文版 第2版》PDF版下载: http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出处:http://blog.csdn.net/jiankunking


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表