java异常有效实践

518 阅读9分钟

java异常有效实践

异常在我们的平时开发过程中是非常寻常并且经常会面对的,我们有很多方式来处理和使用异常。充分发挥异常的优点可以提高程序的可读性,可靠性和可维护性。但是如果使用不当,也会带来很多负面影响。


参考 effective java 第三版中对于异常的一些优秀实践来做一下总结:

No.1 只针对异常的情况才使用异常

异常应该只应用于异常的情况下,永远不要在正常的控制流中使用异常。

例如代码:

int index = 0;
try{
    while(true){
        System.out.println(strList.get(index++));
    }
}catch (ArrayIndexOutOfBoundsException e){}

上图代码的功能是遍历输出strList集合中的全部元素,但其实我们知道只需要一个foreach就能遍历输出 集合中的所有元素。也许可能考虑到了使用正常的foreach会使得每次遍历的时候都要去检查当前遍历索引是否越界,以为该种方式在性能方面会更优于正常的处理方式。实际上该种方式比正常的处理要慢(其中涉及到jvm的优化)。

总而言之,异常是为了在异常情况下使用而设计的,不要在正常的控制流中使用异常。


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

如果期望调用者能够适当的恢复,就应该使用受检异常。

用运行时异常来表明编程的错误。

你所实现的未受检异常都应该是RunTimeException的子类。

要在受检异常上提供方法,以便协助恢复。

不要定义任何既不是受检异常也不是运行时异常的抛出类型。


No.3 避免不必要的使用受检异常

受检异常优点:

不同于返回码和未受检异常,受检异常强迫程序员处理异常的条件,从而增加程序的可靠性

受检异常缺点:

1.如果方法抛出受检异常,则调用该方法者就必须在一个try catch块中对异常进行处理,或者在调用方法中声明抛出异常并让他们传播出去。这给程序员带来了不少的负担。

2.抛出受检异常的方法无法直接在Stream中使用。

对于使用受检异常的情况应该同时满足两个条件:

1.正确的使用该方法或者api的情况下不能防止异常发生

2.一旦产生异常程序员可以采取有效的措施来处理异常

若这两点没有同时成立,则更适合使用未受检异常。

消除受检异常的方法:

1.方法返回一个optional类型的对象,若遇到异常,则只是返回一个0长度的optional(该方法的缺点是由于只返回一个optional,缺少其他信息,若发生异常追查原因会比较困难)

2.把抛出异常的方法拆分成两个方法,第一个方法返回一个boolean值,表明是否应该抛出异常,另一个方法则是该方法的处理逻辑。

3.如果程序员知道调用将会成功或者不介意由于异常导致线程被终止。

合理的使用受检异常可以增加程序的可读性和可靠性,如果过度使用受检异常将会给调用者带来很大的负担。如果调用者无法恢复异常则应该抛出未受检异常。如果希望调用者对异常进行处理,首选应该是返回optional值,只有万一失败时,这些无法提供足够的信息来描述异常则考虑使用受检异常。


No.4 优先使用标准的异常

异常重用,java平台类库提供了一组基本的非受检异常,他们满足了大多数api的异常抛出需求。

最常被使用到的两个异常类型:IllegalArgumentException和IllegalStateException。前者代表非法参数异常,后者表示非法状态异常。可以这么说,所有错误的方法调用都可以归结为非法参数和非法状态。另外还有其他异常类也可以表示为非法参数和非法状态异常 例如:NullPointException IndexOutOfBoundsException等等。

不要直接重用Exception,RunTimeException,Throwable 和 error。可以把这些类看成是抽象类,你无法可靠的测试这些异常,他们是一个方法可能抛出的异常的超类。

如果希望增加更多的失败捕获信息,可以子类化标准异常。没有正当的理由,不应该去编写额外的异常累,而应该使用java提供的标准异常类。

对某一个异常发生的情况可能同时满足多个标准异常规范的场合。比如 在长度为10的数组中去取第11个元素。显然这种情况可以理解参数数值太大(IllegalArgumentException),但是这种异常也可以理解为数组中的元素太少(IllegalStateException)。这里我们可以规范如果没有可用的参数值则使用后者异常类,否则使用前者。


No.5 抛出与抽象对应的异常

如果方法抛出的异常和他执行的任务没有明显的关联,这种情形会使人不知所措。方法将他调用的底层方法异常原封不动的向外抛出,例如在一个获取用户信息的方法中调用了手机号解码方法,而该解码方法刚好发生异常,用户信息方法将其捕捉之后直接抛出,这就会让调用获取用户信息方法的调用者很困惑,因为他们并不知道获取用户信息和解码异常之间的关系,从而导致问题并不好排查,到底是客户端传参数不对还是系统的异常。所有为了避免这个问题,更高层的异常应该捕获底层的异常,同时抛出可按照高层抽象进行解释的异常。这过程也叫异常转译。

有一种特殊的异常转译叫做异常链,即底层放入异常被传到高层的异常,高层的异常提供访问方法还获得底层的异常

try {
    URLEncoder.encode("ds","utf-8");
} catch (UnsupportedEncodingException e) {
    throw new IllegalArgumentException(e);
}

如上,高层异常的构造器将原因传到支持链的构造器,从而当异常发生时高层调用者可以调用异常的相关方法来看到底层的异常,另外在打印异常堆栈信息的时候,这样就可以把底层的异常信息给集成到高层异常中。

异常转译相对于直接将底层异常进行抛出会好很多,但我们不应该滥用。对于底层方法的异常我们首先要做的就是应该避免会发生这种异常,例如在调用之前进行参数校验从而防止异常发生。当然底层方法发生异常时,我们其次想到的应该是在高层方法中悄悄的处理异常,从而将高层方法的调用者和异常进行隔离,使用log对异常进行记录,这样有助于排查问题,又可以将客户端代码和最终用户与异常隔离开来。

如果不能阻止并且处理底层异常的发生,我们应该使用异常转译,只有底层抛出的异常恰好能表述高层执行任务的情况下,可以将底层异常直接进行抛出。


No.6 每个方法抛出的所有异常都要建立文档

始终要单独的申明每一个受检异常,并且利用javadoc的@throws标签,准确的记录下每个异常抛出的条件。并且需要抛出具体的异常类而非异常的基类exception或者throwable。

使用javadoc的@throws标签记录一个方法可能会抛出的未受检异常,但不要使用throws关键字将未受检异常包含在方法申明中。

如果一个类中的许多方法在同样的情况下都会抛出一致的异常,那么在该类的文档注释中应该对这个异常建立文档,而不是为每一个方法建立文档。


No.7 在细节消息中包含失败-捕获信息

为了捕获失败,异常的细节信息应该包含对异常有贡献的所有参数和域的值。不过千万不要在细节中包含密码,密钥等敏感信息。


No.8 努力保持失败的原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败的原子性。

保持失败的原子性:

1.设计一个不可变的对象。

2.在执行操作之前进行必要的参数有效性校验,这使得对象状态被修改之前先抛出适当的异常(调整计算处理的顺序,使得任何可能会失败的计算部分都在对象被修改前发生)。

3.在对象的一份临时拷贝上进行操作,当操作完成后在用临时的拷贝来替换原有的对象。

4.写一段恢复的代码,让他来拦截过程中发生的失败,让对象回到被调用前的状态。

错误通常是不可恢复的,不要在方法抛出assertionError时,不需要努力的去保持失败的原子性。


No.9 不要忽略异常

不要忽略异常,忽略异常很简单,使用一个try 并利用空的catch块就能忽略异常,但是空的catch达不到应有的目的。我们可以把异常认为是火灾而trycatch就像是火警器。当异常发生时我们没有做任何的处理而是将他忽略。这将导致没人会在意到这个异常。当真正有一条异常被注意到的时候也许这个异常影响的范围已经非常大了。

也有一些异常我们是可以忽略的。类似文件读取,fileStream关闭操作,当我们对文件没有做任何处理,并且已经读取到文件中的信息,此时我们可以将异常进行忽略。但是忽略的时候我们应该作出必要的注释说明。并且将catch中的类变量名设为ignore。

总之,通过忽略异常来解决异常是一件极具风险的事情,我们需要认真对待异常,并且十分清楚问题的原因以及产生的影响。