Effective Java 第三版——76. 争取保持失败原子性

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java

Effective Java, Third Edition

76. 争取保持失败原子性

在对象抛出异常以后,一般但愿对象仍然处于定义良好的可用状态,即便失败发生在执行操做中。对于检查异常尤为如此,调用者但愿从检查异常中恢复。通常来讲,失败的方法调用应该使对象处于调用以前的状态。具备此属性的方法称为失败原子性( failure-atomic)。git

有几种方法能够达到这种效果。最简单的方法是设计不可变对象(条目 17)。若是对象是不可变的,则失败原子性是必然的。若是一个操做失败,它可能会阻止建立一个新对象,可是它不会让一个现有对象处于不一致的状态,由于每一个对象的状态在建立时是一致的,而且在建立后不能修改。github

对于对可变对象进行操做的方法,实现失败原子性的最经常使用方法是:在执行操做以前检查参数的有效性(条目 49)。 这致使在对象修改开始以前就会抛出大多数异常。 例如,考虑条目 7中的Stack.pop方法:数组

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

若是取消了初始大小检查,当该方法试图从空栈中弹出元素时,仍然会抛出异常。可是,这会使size属性处于不一致的(负数)状态,致使之后对对象的任何方法调用失败。此外,pop方法抛出的ArrayIndexOutOfBoundsException针对抽象来说是不合适的。(条目 73)。数据结构

实现失败原子性的一种密切相关的方法是对计算进行排序,以便任何可能失败的部分在修改对象的部分以前发生。 在执行部分计算时进行参数检查,此方法是前一个方法的天然扩展。 例如,考虑TreeMap的状况,其元素按照某种顺序排序。 为了向TreeMap添加元素,元素必须是可使用TreeMap的顺序进行比较的类型。 在以任何方式修改tree以前,尝试添加错误键的元素天然会由于在tree中搜索元素失败而致使ClassCastException异常。并发

实现失败原子性的第三种方法是,在对象的临时拷贝上执行操做,并在操做完成后用临时拷贝替换对象的内容。当数据存储在临时数据结构中后,计算能够更快地执行时,这种方法天然会出现。例如,一些排序方法在排序以前将其输入列表拷贝到数组中,以下降访问排序内循环中的元素的成本。这样作是为了提升性能,可是做为一个额外的好处,它确保若是排序失败,输入列表保持不变。性能

实现失败原子性的最后的方法是,编写恢复代码(recovery code),但这种作法并不长用,该代码拦截在操做中发生的失败,并使对象将其状态回滚到操做开始以前的点。 此方法主要用于持久性的(基于磁盘)的数据结构。atom

虽然失败原子性一般是可取的,但它并不老是能够实现的。例如,若是两个线程试图在没有适当同步的状况下并发地修改同一个对象,那么该对象可能会处于不一致的状态。所以,若是假定在捕捉到ConcurrentModificationException以后对象仍然可用,那就错了。错误是不可恢复的,因此方法在抛出AssertionError时,甚至不须要尝试保存失败原子性。线程

即便在可能存在实现失败原子性的状况下,也并不是老是可取的。 对于某些操做,它会显着增长成本或复杂性。 也就是说,一旦你意识到这个问题,一般均可以自由而轻松地作到失败原子性。设计

总之,做为规则,任何生成的异常都是方法规范的一部分,应该使对象处于方法调用以前的状态。 违反此规则的地方,API文档应清楚地指出该对象将保留在哪一种状态。遗憾的是,许多现有的API文档没法实现这一理想。

相关文章
相关标签/搜索