天天看点

【读书笔记】《Effective Java》(8)--异常异常

没想到今天完成了两章笔记的总结,这部分内容感觉没有泛型那部分难懂,不过要想熟练运用还是要多写代码才行,毕竟照以往我的代码,异常处理都是很草率的。

下一部分是并发,这一章有些难度,还是先看看其他博文扫扫盲再看书。

异常

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

  • 异常处理相比于其他代码没有进行更多的优化,甚至会阻止编译器对代码的优化,所以不要将本应该在业务逻辑中处理的控制流放到try-catch中处理
  • 对于设计API来说,设计良好的API不应当强迫它的客户代码为了正常的控制流而使用异常。如果类具有“状态相关”的方法,这个类应当配以“状态测试”方法,以指示是否可以调用这个状态相关的方法;另外一个方法是如果类的实例处于不适当的状态时,调用“状态相关”的方法,返回可识别的值,比如null。
  • 对于上面提到的“状态测试”和返回可识别的值两种做法,权衡的原则是如果存在并发访问的情况,或者“状态检测”方法执行的工作会重复“状态相关”方法的工作,从安全性和性能角度考虑,应当使用返回可识别的值的做法,其他情况应当使用“状态检测”,因为“状态检测”提供了更好的可读性,并且更易于查错。

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

  • Throwable类下有三个子类:受检的异常(checked exception)、运行时异常(run-time exception)以及错误(error)
  • 如何选择抛出的异常类型:
    1. 如果期望调用者能够使当地恢复,应当使用受检的异常
    2. 用运行时异常表明编程错误
  • 一些规则:
    1. Error类按照惯例被JVM保留用于表示资源不足、约束失败、或者其他是程序无法继续执行的条件,最好不要自己扩展Error。所有未受检的可抛出类都应当是RuntimeException的子类
    2. 自定义一个有别于受检异常、运行时异常、错误的可抛出类也是可以的,从行为意义上它们等同于受检异常,但永远不要这么做,这会给使用它们的人带来困惑。

59. 避免不必要地使用受检的异常

  • 使用受检异常的情况:除以下两种情况之外,使用受检的异常被视为是不正确的,应当使用运行时异常
    1. 正确地调用API并不能阻止异常的发生
    2. 一旦出现异常,调用异常的程序员可以立刻采取有用的行为
  • 将受检异常变为运行时异常的方法:将这个抛出异常的方法分成两个 方法,其中一个方法返回boolean,表明是否应该抛出异常。这种方法形式类似于第57条提到的“状态检测”方法,所以同样,“状态检测”方法的问题(并发、性能)也适用于这种方法。

60. 优先使用标准的异常

  • 使用现有异常的好处:
    1. 是客户代码更加易于学习和使用
    2. 可读性更好
    3. 异常类型越少,开销越少
  • 一些常见的异常类型:
    异常类型 使用场合
    IllegalArgumentException 非null的参数值不正确
    IllegalStateExcepition 对于方法调用而言,对象状态不适合
    NullPointerException 在禁止使用null的情况下使用null
    IndexOutOfBoundsException 下表参数值越界
    ConcurrentModificationException 在禁止并发修改的情况下,检测到并发修改
    UnSupportedOperationException 对象不支持用户请求的方法

61. 抛出与抽象相对应的异常

  • 问题:如果方法调用的底层抛出了异常,这个异常和本方法可能没有明显的联系,这会使调用方法的用户感到奇怪。而且,这种情况同样暴露了实现细节,如果以后更改实现方法,异常类型也会随之变动。
  • 本条建议:如果不能阻止或者处理来自底层的异常,更高层的实现应当捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常,这种方法被成为异常转义,但这最好不要滥用。更好的做法是调用底层方法时确保它可以被执行成功(比如传参的有效性),或者高层不抛出底层的异常(这里我理解是高层自己解决掉异常),将其记录,而不将其展示给客户代码。
  • 对于高层包装来自底层的异常并重抛这种异常转义的方法,有一种叫做异常链的形式。通过构造新的异常,并将底层异常传入构造器,防止底层异常丢失,对于没有支持链的异常(自定义的,继承自Throwable的),可以通过initCase方法设置原因。

62. 每个方法抛出的异常都要有文档

  • 始终要单独地声明受检的异常,并利用JavaDoc的

    @throw

    标记,准确记录下抛出异常的条件,不要简单地概括throw Exception,如果一个类中多个方法出于同样的原因抛出了同一个异常,在该类的文档注释而不是方法的注释中为这个异常建立文档是可以的。
  • 对于运行时异常,应当为它们建立起良好的文档,尤其对于接口,因为这份文档构成了该接口的通用约定。虽然这一点(为运行时异常建立文档)并非那么容易遵守。

63. 在细节消息中包含能捕获失败的消息

  • 对于未捕获的异常,系统会调用异常的toString方法打印该异常的堆栈信息,如果toString设计的不够好,而这个异常又难以重现,获取更有用的信息会很难。
  • 本条建议:
    1. 为了捕获失败,异常的细节信息应当包含所有“对该异常有贡献”的参数和域的值。
    2. 应当区分异常的细节和“用户层次的错误信息”,对于调试程序的维护人员,更重要的是信息的内容而不是可读性
    3. 有些异常类(例如IndexOutOfBoundsException,但并不多)提供了使用细节信息的构造方法,对于IndexOutOfBoundsException有

      public IndexOutOfBoundsException(int lowerBound,int upperBound,int index)

      这样的构造器,来确保异常的细节信息包含足够多的能描述捕获失败的消息,应当推荐。

64. 努力使失败保持原子性

  • 失败原子性:指失败的方法调用应当是对象保持在被调用之前的状态。
  • 获得失败原子性的方法:
    1. 检查参数的有效性
    2. 调整计算过程的顺序,是的任何可能会导致失败的计算部分在对象状态被修改之前发生。
    3. 编写恢复代码,让对象回滚到失败之前,但这中方法并不常用,主要用于持久性的数据结构
    4. 对对象的拷贝进行操作,例如,Collections.sort就是在内部将输入列表转到一个数组,还提高了性能
  • 不适合实现失败原子性的情况:此时应当将其记入API文档中,指明失败时对象将在什么状态,是否可用
    1. 并发修改对象导致状态不一致:这种情况下通常是不可恢复的
    2. 保持失败原子性会显著增加开销或者复杂性的时候

65. 不要忽略异常

  • 即使对于不需要捕获的异常,也要写一句注释,标明为什么可以忽略这个异常
  • 特殊的情况:关闭FileInputStream时,可以忽略。因为没有改变文件状态,因此我们不必进行任何恢复操作;而且我们已经从文件中读取所需信息,因此不需要终止进行的操作。即使此种情景,记录异常信息也是明智的.只要发生异常,你可以调查异常原因。(这句话我没动,我搜索了下,并没有搜到相关的解释,单单写下来,以供记录)
上一篇: OVN实践