效率编程 之「异常」

时间:2022-06-22
本文章向大家介绍效率编程 之「异常」,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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

顾名思义,异常应该只用于异常的情况下;它们永远不应该用于正常的控制流。同理,设计良好的 API 不应该强迫它的客户端为了正常的控制流而使用异常。

Java 程序设计语言提供了三种可抛出结构:受检的异常(checked exception),运行时异常(run-time exception)和错误(error)。在决定使用受检的异常或是未受检的异常时,主要原则是:如果期望调用者能够恰当地恢复,对于这种情况就应该使用受检的异常。通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。

受检异常是 Java 程序设计语言的一项很好的特性。与返回代码不同,它们强迫程序员处理异常的条件,大大增加了可靠性。但是,过分使用受检异常会使 API 使用起来非常不方便。如果方法抛出一个或者多个受检的异常,调用该方法的代码就必须在一个或者多个catch块中处理这些异常,或者它必须声明它抛出这些异常,并让它们传播出去。无论哪一种方法,都给程序员增添了不可忽视的负担。

运行时异常是用来表示编程错误的。我们实现的所有未受检的抛出结构都应该是RuntimeException直接或者间接的子类。总而言之,对于可恢复的情况,使用受检异常;对于编程错误,则使用运行时异常。

第 2 条:优先使用标准异常以及抛出与抽象相对应的异常

专家级程序员与缺乏经验的程序员一个最主要的区别在于,专家追求并且通常也能够实现高度的代码重用。代码重用是值得提倡的,这是一条通用的规则,异常也不例外。Java 平台类库提供了一组基本的未受检的异常,它们满足了绝大多数 API 的异常抛出需要。因此,我们应该优先使用标准异常。

  • 最经常被重用的异常是IllegalArgumentException,当调用者传递的参数值不合适的时候,往往就会抛出这个异常。
  • 另一个经常被重用的异常是IllegalStateException,如果因为接收对象的状态而使调用非法,通常就会抛出这个异常。例如,调用非被初始化的对象。
  • 另一个值得了解的通用异常时ConcurrentModificationException,如果一个对象被设计为专用于单线程或者与外部同步机制配合使用,一旦发现它正在(或已经)被并发地修改,就应该抛出这个异常。
  • 最后一个值得注意的通用异常是UnsupportedOperationException,如果对象不支持所请求的操作,就会抛出这个异常。

如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由底层抽象抛出的异常时,往往会发现这种情况。为了避免这个问题,更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常。这种方法称之为“异常转译”,如下所示:

try {
    // User lower-level abstraction to do our biding
} catch(LowerLevelException e) {
    throw new HighLevelException(...);
}

一种特殊的异常转译形式称为异常链,如果底层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。底层的异常(原因)被传到高层的异常,高层的异常提供访问方法(Throwable.getCause)来获得底层的异常:

try {
    // User lower-level abstraction to do our biding
} catch(LowerLevelException e) {
    throw new HighLevelException(e);
}

高层异常的构造器将原因传到支持链的超级构造器,因此它最终被传给Throwable的其中一个运行异常链的构造器。大多数标准的异常都有支持链的构造器。对于没有支持链的异常,可以利用ThrowableinitCause方法设置原因。异常链不仅让我们可以通过程序(用getCause)访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中。尽管异常转译与不加选择地从底层传递异常的做法相比有所改进,但是它也不能被滥用。

总而言之,如果不能阻止或者处理来自更底层的异常,一般的做法是使用异常转译,除非底层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从底层传播到高层。异常链对高层和底层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析。

第 3 条:努力使失败保持原子性以及不要忽略异常

一般而言,失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性。要想使方法具有失败原子性,以下几种途径可以实现这种效果:

  • 最简单的方法莫过于设计一个不可变的对象,如果一个操作失败了,它可能会阻止创建新的对象,但是永远也不会使已有的对象保持在不一致的状态之中;对于在可变对象上执行操作的方法,获得失败原子性最常见的方法是,在执行操作之前检查参数的有效性,这可以使得在对象的状态被修改之前,先抛出异常。
  • 一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。如果对参数的检查只有在执行了部分计算之后才能进行,这种办法实际上就是上一种办法的自然扩展。
  • 第三种获得失败原子性的办法远远没有那么常用,做法是编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。这种办法主要用于永久性的(基于磁盘)的数据结构。
  • 最后一种获得失败原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果替代对象的内容。如果数据保存在临时的数据结构中,计算过程会更加迅速,使用这种办法就是件很自然的事。

而且,我们不应该忽然异常,例如:

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

如上述代码所示,空的catch块会使异常达不到应用的目的,即强迫我们处理异常的情况。至少,catch块也应该包括一条说明,解释为什么可以忽略这个异常。此外,错误(相对于异常)通常是不可恢复的,当方法抛出错误时,它们不需要努力保持失败原子性。