第69条 只针对异常的情况才使用异常
69.1 使用异常完成流程控制的问题
代码
try {
int i = 0 ;
while ( true )
range[ i++ ] . climb ( ) ;
} catch ( ArrayIndexOutOfBoundsException e) {
}
for ( Mountain m : range)
m. climb ( ) ;
基于异常的数组循环的问题
异常设计的初衷是用于不正常的情况,所以几乎没有JVM实现会对它们进行优化 将代码放在try-catch块中反而会抑制JVM实现中,可能执行的某些优化 对数组进行遍历的标准写法,其实并不会导致冗余的检查,很多JVM实现会对这方面进行优化 在作者的机器上,基于异常的写法要比标准的写法,慢上两倍 基于异常的写法模糊了代码的意图、降低代码性能、执行可能会出现问题,例如如果climb方法中也会产生数组越界异常,标准写法下,会直接抛出异常,打印栈信息,但在基于异常的写发下,这个抛出的数组越界,会被系统当做是循环range结束,然后代码继续正常执行
69.2 API设计
设计良好的API不应该导致它的客户端只能通过使用异常来完成流程控制 "状态相关"方法:只有在特定的不可预知的状态下才可以被调用的方法 "状态测试"方法:检测当前状态是否可以执行"状态相关"方法 类具有状态相关方法,就应该同时具备状态测试方法,不然可以想象,客户端只能通过状态相关方法产生的异常,才能判断出其状态变化,控制器执行状态变化后的代码 例如Iterator接口,next为状态相关方法,hasNext为状态测试方法
for ( Iterator< Foo> i = collection. iterator ( ) ; i. hasNext ( ) ; ) {
Foo foo = i. next ( ) ;
. . .
}
try {
Iterator< Foo> i = collection. iterator ( ) ;
while ( true ) {
Foo foo = i. next ( ) ;
. . .
}
} catch ( NoSuchElementException e) {
}
可以不使用状态测试方法,修改将状态相关方法,不再抛出异常,而是在特定状态下,返回一个空的Optional或返回一个可识别的返回值,比如null
如果调用状态相关方法的对象,(比如上面Iterator类型的i)将在缺少外部同步的情况下被并发访问,或其状态可被外界所改变时,就应该使用Optional或返回可识别返回值null,这两种方式,因为在状态访问和状态测试方法之间,对象状态有可能被改变 状态测试方法比Optional或返回可识别返回值,可读性更强,对于使用不当的情形可能更加易于检测和改正
如果忘了调用状态测试方法,状态相关方法会抛出异常 如果忘了检查可识别的返回值,Bug就很难被发现 Optional要求必须检查,并提供为空时的默认值,所以也可以对不当情形检测和改正
69.3 最佳实践
不要使用异常来完成异常控制 也不要编写那种导致客户端,必须使用异常来完成流程控制的API
第70条 对可恢复的情况使用受检异常,对编程错误使用运行时异常
70.1 Throwable的几种类型
java提供了三种可抛出的结构,受检异常(checked exception),运行时异常(run-time exception)、错误(error)。error和run-time exception,一起称为非受检异常 如果期望调用者能够适当的恢复,使用受检异常
受检异常,必须try-catch,或者方法声明中,将其抛出,它强制用户从这个异常中恢复 受检异常,表示告诉API用户,与这个抛出的异常相关联的条件,是调用这个方法的一个可能结果 用运行时异常表明编程错误
运行时异常,表示违反了该方法的前提条件,例如数组访问约定指明数组的下标必须在0和长度-1之间,ArrayIndexOutOfBoundsException就表示客户违反了这个前提条件 有时一个异常情况,是应该被作为受检异常(需要其恢复),还是应被当做编程错误并不是很清晰,需要API设计者人为判断,如果认为这种情况可以恢复,就用受检异常,如果觉得不应被恢复,就使用运行时异常,如果实在判断不了,使用未受检异常,原因参考item 71 按照惯例,错误(error)往往被JVM本身使用,用于表明资源不足、约束失败,或者其他使程序无法继续执行的条件,所以自己写代码定义非受检异常时,不要使用Error。也就是说自己定义的所有非受检异常,都应该是RuntimeException的子类,不应该定义Error子类,也不应该抛出AssertError异常 虽然可以直接通过继承Throwable来定义一个可抛出结构(throwable),但这种方式定义出来的可抛出结构行为等同于受检异常,因此永远不要这样做,这样做只会让使用这个API的用户迷惑 异常中可以定义一些方法,这些方法的主要用途是获取引发这个异常的条件信息(原因),如果没有这些方法,就需要自己解析"该异常的字符串表示法",也就是异常对象的toString返回的字符串,但对于不同版本、不同实现下的字符串表示法都大相径庭,自己编写对其进行解析的代码,移植性非常差,而且非常脆弱
对于受检异常,一般都想恢复,所以这种辅助方法尤其重要,通过这些方法,调用者可以获得一些有助于恢复的信息 例如假设用户资金不足导致购买商品失败,抛出一个受检异常,那么这个异常对象应该提供一个辅助方法,返回客户差的金额,客户端就可以方便地使用这个方法,更多用法参考item 75 异常的字符串表示法:就是这个异常对象的toString方法的返回值,比如对于java.lang.ArrayIndexOutOfBoundsException: 1,你需要自己写方法,解释这个字符串,“数组越界了,使用了下标为1的元素”
70.2 最佳实践
可恢复的情况,抛出受检异常,对于编程错误抛出运行时异常,不确定是否可以恢复,抛出未受检异常 不要定义既不是受检异常,又不是运行时异常的Throwable子类 在受检异常上应该提供辅助方法,方便其恢复
第71条 避免不必要地使用受检异常
71.1 受检异常的问题
受检异常强迫程序员必须try-catch,或向上throw这种异常,提升了程序可靠性,但过分使用异常,会使API使用起来非常不方便 java8这个版本中,Stream中想抛出受检异常非常麻烦
public static void main ( String[ ] args) throws IOException{
Stream. of ( "a" , "b" , "c" ) . forEach ( str - > {
throw new IOException ( ) ;
} ) ;
}
public static void main ( String[ ] args) {
Stream. of ( "a" , "b" , "c" ) . forEach ( str - > {
try {
throw new IOException ( ) ;
} catch ( Exception e) {
;
}
} ) ;
}
public static void main ( String[ ] args) throws IOException{
Stream. of ( "a" , "b" , "c" ) . forEach ( str - > {
throw new RuntimeException ( new IOException ( ) ) ;
} ) ;
}
当客户端按要求使用API,却还是能够产生某种异常,或者某种异常一旦产生,希望被使用API的程序员人工干预如何处理时,这个异常就应该使用受检异常。如果以上两点都不符合,就可以使用非受检异常 这里作者说抛出一个受检异常比抛出两个更麻烦,意思其实是,如果原来有受检异常,那么多抛一个,不会造成太大的编程负担,加个catch块就行了,但如果原来一个没有,突然增加一个受检异常,这种会导致客户端编程负担变化更大。而不是说两个受检异常比一个受检异常好
71.2 消除受检异常
方法一:使用返回一个空的Optional来替代抛出受检异常,但这种返回空Optional的方法,没办法返回该方法无法正确执行任务的原因。反观抛出受检异常的方法,不仅你可以通过抛出的异常类型来判断方法无法正确执行的原因,又可以利用异常的一些辅助方法,来得到一些额外信息(item 70) 方法二:将受检异常转为非受检异常,这种方式不一定总适合,但这样做会使API更容易使用。这种做法类似于item 69中提出的状态测试方法
将抛出受检异常的方法分成两个方法 一个返回boolean值,表示是否应该抛出该异常
try {
obj. action ( args) ;
} catch ( TheCheckedException e) {
. . .
}
if ( obj. actionPermitted ( args) ) {
obj. action ( args) ;
} else {
. . .
}
obj. action ( args) ;
71.3 最佳实践
少量、谨慎地使用受检异常,可以提升程序的可靠性,过度使用会使API难以使用 如果调用者压根没法让客户端从调用的API产生的异常中恢复,API设计时,就不应该抛出受检异常,而是应产生非受检异常 即使可以恢复,而且你又希望客户端强制对其恢复,API也应该首选编写返回空Optional的方法,而不是抛出受检异常的方法 只有在客户端需要从失败的API中获取足够的信息时(例如失败原因等),您才应该抛出一个checked异常
第72条 优先使用标准的异常
72.1 标准的异常分类
专家级程序员与缺乏经验的程序员最主要区别在于专家通常追求,而且能够实现高度的代码重用,代码重用是很重要的,异常自然也应该重用
重用异常使API更容易学习和使用,因为大家都知道这个异常什么意思 对于用到这些API的程序,可读性更强,因为不会出现很多程序员不熟悉的异常 异常类越少,占用内存越少,加载异常类的开销越少(这条不重要,省不了多少内存) IllegalArgumentException:调用者传递的参数不适合时,抛出该异常。比如一个参数代表"某个动作重复次数",程序员如果给这个参数传递了一个负数,就应该抛出该异常 IllegalStateException:调用该方法的对象的状态不正确时,抛出该异常。例如某对象正确初始化之前就调用其方法A,那么方法A就应该抛出该异常 其实所有错误的方法调用,都可以归结为IllegalArgument(非法参数)或IllegalState(非法状态),其他一些标准异常,一般都表示某种特定情况下的非法参数或非法状态 特定情况的非法参数
NullPointerException:为某个不允许为空的参数值,传递null值时抛出 IndexOutOfBoundsException:为序列的下标的参数值传递了越界的值 特定情况的非法状态
ConcurrentModificationException:专门设计用于单线程的类的对象,如果被检测到,它被并发的修改,就应该抛出该异常。这个异常一般不会被真正抛出,它一般都是作为一个提示,因为不太可能可靠地检测到并发的修改 UnsupportedOperationException:如果对象不支持所请求的操作,抛出这个异常。很少使用,因为很少会出现一个对象可以调用某个方法, 而这个对象又不支持这个方法的
一般用于反射时没有对应的方法 或类没有实现由它们实现的接口所定义的所有可选操作。例如自定义一个只能添加元素的List实现,那么其remove方法,就应该抛出这个异常 public class MyList extends ArrayList {
@Override
public Object remove ( int index) {
throw new UnsupportedOperationException ( ) ;
}
}
不要直接重用(使用)Exception,RuntimeException,Throwable,或Error。对待这些异常应该就像对待抽象类一样 其他的一些异常,比如关于算数的ArithmeticException和NumberFormatException,但要注意,选择重用异常时,这个异常的文档介绍应该符合你的要求,而不是简单的看这个异常的字面意思,觉得符合,就去选择它 如果想让返回的异常对象,可以新增方法来提供更多细节,可以新建一个异常类,这个类继承标准异常类,但异常是可序列化的,所以如果没有非常正当的理由,不要自己编写异常类(第12章详细讲述了原因) 选择重用哪种异常并不总是非常明确,比如一个纸牌对象,有一个发牌方法,该方法有一个参数表示本次发的牌的数量,如果调用者为这个参数传递的值,大于剩余纸牌数,既可以认为是参数值过大(参数的问题),从而抛出IllegalArgumentException,也可以认为是纸牌对象剩余的牌太少(对象的问题),从而抛出IllegalStateException。一般这种情况,如果无论为这个方法传递什么参数,都没发让这个方法正确工作,就选择抛出IllegalStateException,否则抛出IllegalArgumentException
第73条 抛出与抽象对应的异常
73.1 异常转译
如果方法抛出的异常,与它所执行的任务没有明显的联系,这种情形会让人不知所措。 当一个低层的方法抛出的异常,传递给了高层的方法,然后又通过这个高层的方法抛出时,就会出现这种情况,这种情况不但使人困惑,而且会导致外层API被污染,可以想象客户端会try-catch这些低层的方法所抛出的异常,但一旦高层的API在后续发行版本中改变了,抛出了新的种类的异常,那么客户端代码也要进行修改 为了避免这种问题,高层的方法,应该捕获底层方法抛出的异常,并重新抛出一个客户端可以理解的,高层方法应抛出的异常,这种做法称为异常转译(exception translation)
try {
lowerFunction ( ) ;
} catch ( LowerLevelException e) {
throw new HigherLevelException ( . . . ) ;
}
public E get ( int index) {
try {
return listIterator ( index) . next ( ) ;
} catch ( NoSuchElementException exc) {
throw new IndexOutOfBoundsException ( "Index: " + index) ;
}
}
73.2 异常链
如果高层方法中,需要获取低层方法中所抛出的异常对象,比如低层抛出的异常对调试导致高层异常的问题非常有帮助的情况,可以使用异常链来处理。高层方法抛出的异常的getCause方法(这个方法其实来自Throwable),就可以获取到底层的异常对象 高层方法抛出的异常类的定义
class HigherLevelException extends Exception {
HigherLevelException ( Throwable cause) {
super ( cause) ;
}
}
异常链的创建
try {
lowerFunction ( ) ;
} catch ( LowerLevelException cause) {
throw new HigherLevelException ( cause) ;
}
item 72中介绍的那些标准的异常一般都有chaining-aware构造器,少数那些没有的,你可以通过调用Throwable的initCause方法,将低层方法的异常对象传递给高层方法的异常对象,从而完成异常链 异常链不仅可以让你通过程序来获取低层方法抛出的异常,也可以将低层方法的异常的异常栈信息,集成到高层方法的异常的异常栈中 虽然异常转译比起无脑地将低层方法的异常传播出去,要好的多,但也不能滥用。
如果有可能,处理来自低层方法的异常的最好的做法,是调用低层方法前确保他们会执行成功,从而避免它们抛出异常。例如可以在给低层方法传递参数前,检查更高层方法的参数有效性,从而避免低层方法抛出异常(这样就不用对低层方法进行try-catch了,自然也不用向外抛了) 如果确实无法阻止低层方法抛出异常,尽量在高层代码中就对这个异常进行处理(try-catch捕获后恢复),并用日志记录下来,这样有助于管理员调查问题,同时将客户端代码与这个异常进行隔离
73.3 最佳实践
如果不能阻止或处理来自底层级方法的异常,一般做法是使用转译 只有在低层方法的规范碰巧可以保证"它所抛出的所有异常对于更高层也是合适的"情况下,才可以将异常低层传播到高层 异常链可以抛出适当的高层方法合适的异常的前提下,又允许客户端捕获低层方法抛出的异常,从而进行失败分析
第74条 每个方法抛出的所有异常都要记入文档
74.1 将异常信息记入文档
始终要单独地声明受检异常,即不能将两个受检异常合并成一个大异常声明,并且利用Javadoc的@throws标签,准确地记录下引发每个受检异常的所有条件 直接声明抛出一个大异常例如throws Exception或throws Throwable,会掩盖该方法可能抛出的具体异常种类,导致客户不知道如何使用它,除了main方法外,不要这样做,main方法不受这个限制是因为main方法不会被客户端调用,只会被虚拟机本身调用 java语言并没有要求对未受检的异常进行声明,但和受检异常一样,我们都应该对他们建立文档
未受检异常通常代表编程上的错误(item 70),在文档中写明这些错误的前提条件,就可以让其他程序员了解这些错误,从而帮助他们避免犯类似错误 将方法可能抛出的所有未受检异常组织成一个好的文档,就可以有效地描述该方法能够执行成功的所有前提条件 对于接口中的方法,在文档中记录下它可能抛出的未受检异常尤其重要,因为这个文档就相当于该接口的通用约定的一部分,也就是说该方法的具体实现应该能且只能抛出该未受检异常 不要使用throws关键字将未受检的异常包含在方法声明中,因为你的API使用者对方法抛出的受检异常与非受检异常的处理是完全不同的,所以方法的声明,必须让使用者一眼就分辨出哪些是该方法能抛出的受检异常,哪些是非受检异常。程序员一般认为那些在@throws标签中存在,而throws声明中不存在的异常就是非受检异常,如果在throws中声明非受检异常,会让人误会这是个受检异常 在实践中,可能无法做到为一个方法的所有非受检异常都建立文档,因为类一中的方法调用了类二中的方法,类一的编写者为其所有可能抛出的非受检异常都建立了文档,但一旦此时类二中方法的实现发生了变化,抛出了新的非受检异常,这时类一也会抛出这个新的非受检异常,但文档却没有改变 如果一个类中很多方法,处于同一个原因,抛出同一种异常,可以在类级文档注释中,对其进行注释,而不需要在方法级文档注释中,对其注释
74.2 最佳实践
为编写的每个方法所能抛出的每个异常都建立文档,无论是抽象方法还是具体方法,无论是受检异常还是非受检异常 异常的注释,在文档中应该使用@throws标签完成 要在方法的throws子句中,为每个受检异常提供单独的声明,不能合成一个大异常来声明 不要通过throws子句声明未受检异常 如果没有为可抛出的异常建立文档,API的使用者就很难或根本无法有效地使用你开发的API
第75条 在细节消息中包含失败-捕获信息
字符串表示法(string representation):就是异常对象的toString方法返回的字符串 细节消息(detail message):字符串表示法中,先是该异常的类名,紧随其后的就是细节消息 失败-捕获信息(failure-capture information):异常对象收集(capture)到的有关失败原因(failure)的信息
75.1 在细节消息中包含失败-捕获信息的总体原则
细节消息是程序员在调查程序失败原因时,一定会检查的消息 如果失败的情形不容易重现,想要获得更多失败-捕获信息就非常困难 因此应该在细节消息中包含尽可能多的失败-捕获信息 为了捕获失败的信息,细节消息中应该包含所有引起这个异常的参数和属性值,例如IndexOutOfBoundsException的细节消息中,应该包含下界、上界、没有落在界内的下标值。因为这三个值任何一个有问题都可能是引发这个异常的原因,如果下标没在界内,那就是下标的问题,如果是下界比上界都要大,那就是界限自身的问题,在细节消息中记录这些值,就能让程序员快速定位错误原因,加快诊断过程 不要在细节消息中包含密码、密钥等私密信息,因为这些会打印到堆栈轨迹中,而很多人都能看到这些堆栈轨迹 异常的细节消息不应该与用户层级的错误消息混为一谈,细节消息是给程序员分析失败原因用的,用户层级错误消息是给用户提示错误原因的,也就是说用户必须能看懂。所以细节消息的内容比可读性重要,而用户层级错误消息可读性要比内容重要 为了确保细节消息中包含足够的失败-捕捉信息,可以使用所有需要的信息,建立一个异常的构造器,使用这个构造器来创建异常,就能保证所有需要信息都被传入到异常
public IndexOutOfBoundsException ( int lowerBound, int upperBound, int index) {
super ( String. format ( "Lower bound: %d, Upper bound: %d, Index: %d" , lowerBound, upperBound, index) ) ;
this . lowerBound = lowerBound;
this . upperBound = upperBound;
this . index = index;
}
java9新提供了一个包含一个类型为int的index参数的构造器,但没要求该构造器传入lowerBound和upperBound,Java平台类并没有广泛使用这种做法, 但这种做法仍然值得大力推荐,这样做可以令程序员很容易的进行失败-捕获。而且这种用法将代码集中在异常类中以生成高质量的详细消息,而不是要求类的每个用户都编写代码生成详细消息 应该为异常提供可以获取其失败-捕获信息的辅助方法(item 70),为受检的异常提供这些辅助方法尤其重要,因为失败-捕获信息对于从失败中恢复非常有用。虽然程序员很少需要在程序中通过辅助方法来查看非受检方法中的细节,但根据一般原则为非受检方法提供这些辅助方法也是明智的
第76条 努力使失败保持原子性
失败原子性:失败的方法调用,能够使调用它的对象仍然保持在调用该方法之前的状态,就说该方法具有失败原子性
76.1 令方法具有失败原子性的做法
调用方法的对象本身不可变,那么其方法一定具有失败原子性,因为不可变对象的状态根本无法改变 对于可变对象上的方法,可以在执行方法中的改变对象状态的操作前,检查参数有效性,发现无效的参数就抛出异常,从而获得失败原子性
调整方法内部的处理顺序,将使得任何可能会失败的计算都发生在对象状态被修改之前,这种做法和上面的类似。例如为TreeMap对象添加元素时,会先检查加入的元素是否可以与其内其他元素进行比较,如果不能就抛出ClassCastException,检查成功后,才会真正修改TreeMap对象的内部状态
public Object pop ( ) {
if ( size == 0 )
throw new EmptyStackException ( ) ;
Object result = elements[ -- size] ;
elements[ size] = null;
return result;
}
在调用方法的对象的一份临时拷贝上,进行状态的修改,当操作完全成功后,再用拷贝对象,来替代原对象并返回。有些排序函数会在执行排序前,先把传入它的list放入到一个数组内,以便降低在排序的内循环中访问元素所需要的的开销,本身这样做是处于性能考虑,但其实同时带来了另一个好处,就是即使排序失败,也能保持传入的list对象保持原状态 最不常用的方法:编写一段恢复代码(recovery code),由它拦截操作过程中发生的失败,并将对象回滚到整个操作发生前的那个状态。这种方法一般用于恢复持久化(基于磁盘)的数据结构
76.2 无需保证失败原子性的情况
无法保证失败原子性
多个线程在没有适当同步机制情况下,并发修改某个对象,这个对象就可能处于一个相互矛盾的状态,所以说假设一个方法抛出了ConcurrentModificationException,我们还假设其内修改的对象仍然是可用的,这本身就不正确,因此也就谈不上保证失败原子性 Errors本身就是不可恢复的,所以当方法抛出一个AssertionError时,你也不应该尝试恢复调用该方法的对象的状态 能保证失败原子性,但不希望这样做
为了达成失败原子性会大大地增加性能开销和编程复杂度
76.3 最佳实践
保证失败原子性,应该当成是方法的规则一样来遵守 如果方法没能做到失败原子性,API文档就应该清楚地指明一旦发生异常,调用该方法的对象会处于什么状态 大量现有的API都没能做到这一点
第77条 不要忽略异常
77.1 忽略异常
忽略异常:catch捕获到异常后,内部什么都不处理即可
try {
. . .
} catch ( SomeException e) {
}
77.2 允许忽略异常的情况
关闭FileInputStream时抛出的异常可以忽略,由于你并没有修改文件的状态(不是FileOutputStream),所以不必执行任何恢复动作,同时由于你已经从文件中读取到了所需信息,因此也不必终止当前的操作,既不必恢复,也不必终止,因此可以选择忽略这个异常 即使可以忽略异常,也还是应该把异常记录下来,因为如果这个异常经常放生,记录该异常可以方便后续的原因调查 忽略异常时,catch块中应该包含一条注释,说明为什么可以这样做,并且将异常变量命名为ignored
Future< Integer> f = exec. submit ( planarMap: : chromaticNumber) ;
int numColors = 4 ;
try {
numColors = f. get ( 1 L, TimeUnit. SECONDS) ;
} catch ( TimeoutException | ExecutionException ignored) {
}
77.3 最佳实践
无论受检异常还是非受检异常,都不应该用空的catch块来忽略它 忽略异常会导致程序在遇到错误后,仍然悄然地执行,然后可能在将来的某个点上,当程序再不能容忍上面那个错误的时候,执行失败,此时难以查找问题原因 正确地处理异常可以避免失败,即使无法处理,至少将异常直接抛出,这样程序可以立即失败,同时又可以保存信息以帮助调试故障