第五十七条、只针对异常的情况才使用异常
-
不要优先使用基于异常的模式:
- 因为异常机制的设计初衷是用于不正常的情况,所以很少会有JVM实现对它们进行优化,使得与显式的测试一样快速;
- 把代码放在try-catch块中反而组织了现代JVM实现本来可能要执行的某些特定优化;
- 对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将他们优化掉。
异常应该只用于异常的情况下,它们永远不应该用于正常的控制流。
-
设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。如果类具有状态相关的方法,即只有在特定的不可预知的条件下才可以调用的方法。
这个类也应该有单独的状态测试方法,即指示是否可以调用这个状态相关的方法。
或者如果状态相关的方法被调用时,该对象处于不适当的状态中,它就会返回一个可识别的值,如null。
-
对于“状态测试方法”和“可识别的返回值”两种做法如何选择:
- 如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,使用可被识别的返回值可能是很有必要的。因为在调用“状态测试”方法和调用对应的“状态相关”方法的时间间隔中,对象的状态可能发生变化。
- 如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能的角度考虑,就应该使用可被识别的返回值。如果所有其他方面都是等同的,那么“状态测试方法”则略优于可被识别的返回值。因为它提供了更好的可读性,对于使用不当的情形可能更加容易检测和改正。
第五十八条、对可恢复的情况使用受检异常,对编程错误使用运行时异常
Java程序设计语言提供了三种可抛出结构:受检的异常(checked exception)、运行时异常(run-time exception)和错误(error)。关于在什么时候抛出何种结构,有些一般性的原则可以指导。
如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常。通过抛出受检的异常,强迫调用者在一个catch字句中处理该异常,或者将它传播出去。因此,方法中声明要抛出的每个受检的异常,都是对API用户的一种潜在指示:与异常相关联的条件是调用这个方法的一种可能结果。
-
另外两种未受检的可抛出结构:运行时异常和错误。在行为上两者是等同的:它们都是不需要也不应该被捕获的可抛出结构。这种情况下属于不可恢复的情形,继续执行下去有害无益。如果程序没有捕捉到这样的可抛出结构,将会导致当前的线程停止halt,并出现适当的错误信息。
- 用运行时异常来表明编程错误。大多数的运行时异常都表示前提违例(precondition violation)即API的客户没有遵守API规范建立的约定。
- 错误类型往往被JVM保留用于表示资源不足、约束失败,或者其他使程序无法继续执行的条件。由于这已经是个几乎被普遍接受的惯例,所以最好不要再实现任何新的Error子类,自己实现的所有未受检的抛出结构都应该是RuntimeException的子类。
对于可恢复的情况使用受检异常,对编程错误使用运行时异常。异常完全意义上也是个对象,可以在它上面定义任何的方法。这些方法的主要用途是为捕获异常的代码而提供额外的信息,特别是引发这个异常条件的信息。
第五十九条、避免不必要地使用受检的异常
受检的异常是Java程序语言的一项很好的特性,它们强迫程序员处理异常的条件,大大增强了可靠性。但是,过分地使用受检的异常会使API使用起来非常不方便。如果一个方法抛出一个或者多个受检的异常,调用该方法的代码必须在一个或多个catch块中处理这些异常,或者它必须声明它抛出这些异常,并将它们传播出去,给程序员增添了负担。
何时使用受检的异常:如果正确地使用API并不能阻止这种异常条件的产生,一旦产生异常,使用API的程序员可以立即采取有用的动作。两种条件缺一不可。
第六十条、优先使用标准的异常
Java平台类库提供了一组基本的未受检异常,它们满足了绝大多数API的异常抛出需要。
-
重用现有异常的好处:
- 它使你的API更容易学习和使用;
- 对于用到这些API的程序而言,它们的可读性更好;
- 异常类越少,意味着内存印迹越小,装载这些类的时间开销越小。
-
常用的异常:
-
IllegalArgumentException
:当调用者传递的参数不合适的时候,往往会抛出这个异常; -
IllegalStateException
:如果因为接收对象的状态而使调用非法,通常会抛出这个异常; -
NullPointerException
:禁止使用null的情况下参数值为null; -
IndexOutOfBoundsException
:下标参数越界; -
ConcurrentModificationException
:在禁止并发修改的情况下,检测到对象的并发修改; -
UnsupportedOperationException
:对象不支持用户请求的方法。
-
第六十一条、抛出与抽象相对应的异常
-
更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法成为异常转译exception translation。
try{ ... }catch(LowerLevelException e){ throw new HigherLevelException(...); }
一种特殊的异常转译的形式称为异常链,低层的异常原因被传到高层的异常,高层的异常提供访问方法来获得低层的异常。大多数标准的异常都有支持链的构造器,对于没有支持异常链的异常,可以利用Throwable的initCause方法设置原因。异常链不仅让你可以通过程序访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中。
异常转译不能滥用,如有可能,处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。如果无法避免低层异常,次选方案是让高层悄悄绕过这些异常,从而将高层方法的调用者与低层的问题隔离开,在这种情况下,可以用适当的记录机制(如:java.util.logging)将异常记录下来。
总结:如果不能阻止或者处理来自更低层的异常,一般的做法是异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从底层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析。
第六十二条、每个方法抛出的异常都要有文档
始终要单独地声明受检的异常,并且利用Javadoc的
@throws
标记,准确地记录下抛出每个异常的条件。永远不要声明一个方法throws Exception:这样的方法声明不进没有为程序员提供“关于这个方法能够抛出哪些异常”的任何指导信息,并且大大妨碍了该方法的使用,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。未受检的异常通常代表编程上的错误,让程序员了解所有这些错误都有利于帮助他们避免继续犯错。使用
@throws
标签记录下一个方法可能抛出的每个非受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。使用API的程序员需要知道哪些是受检的哪些是未受检的。但是为每个方法可能抛出的所有未受检的异常建立文档是很理想的,实践中并未总能做到这一点。如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档是可以接受的。
第六十三条、在细节消息中包含能捕获失败的信息
当程序由于未被捕获的异常而失败的时候,系统会自动地打印出该异常的堆栈轨迹,在堆栈轨迹中包含该异常的字符串表示法,即它的toString方法的调用结果。它通常包含该异常的类名,紧随其后的是细节消息(detail message)。异常的细节消息应该能够捕获住失败,便于以后分析。
异常的细节消息应该包括所有“对该异常有贡献”的参数和域的值。但是不应该太过冗余。异常的细节消息不应该与“用户层次的错误信息”混为一谈,后者对于最终用户而言必须是可理解的。异常的字符串表示法主要是让程序员或者是域服务人员来分析失败的原因。因此,信息的内容比可理解性要重要得多。
方法:在异常的构造器而不是字符串细节消息中引入这些信息,然后有了这些信息,只要把它们放入消息描述中,就可以自动产生细节信息。
第六十四条、努力使失败保持原子性
一般,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法称为具有失败原子性。
-
实现的几种途径:
- 最简单的方法:设计一个不可变的对象。(失败原子性是必然的)
- 对于可变对象执行操作的方法,在执行操作之前检查参数的有效性,这可以使在对象的状态被修改之前先抛出适当的异常。
- 一般的方法:调整计算处理过程的顺序,使得任何有可能失败的计算部分都在对象状态被修改之前发生。
- 编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。(主要用于永久性的(基于磁盘的)数据结构)
- 在对象的一份临时拷贝上执行操作,当操作完成后再用临时拷贝的结果代替对象的内容。例如:
Collections.sort
在执行排序之前,首先把它的输入列表转入到一个数组中,以便降低在排序的内循环中访问元素所需要的开销。
一般情况下都希望实现失败原子性,但是并非总是可以做到的。例如两个线程企图在没有适当的同步机制的情况下,并发地修改同一个对象。错误(相对于异常)通常是不可恢复的,当方法抛出错误时,它们不需要努力保持失败原子性。
第六十五条、不要忽略异常
当API的设计者声明一个方法将要抛出某个异常时,他们等于在试图说明某些事情,所以不要用一个空的catch块来忽略它。会使异常得不到应有的目的。
有一条情况可以忽略:即关闭FileInputStream的时候,因为还没有改变文件的状态,因此不必执行任何恢复动作,并且文件中读取到所需要的信息,因此不必终止正在进行的操作,但是此时把异常记录下来是一个明智的做法,可以因此调查异常的原因。