Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。
76. 争取保持失败原子性
在对象抛出异常之后,通常希望对象仍然处于定义良好的可用状态,即使失败发生在执行操作中。对于检查异常尤其如此,调用者希望从检查异常中恢复。一般来说,失败的方法调用应该使对象处于调用之前的状态。具有此属性的方法称为失败原子性( failure-atomic)。
有几种方法可以达到这种效果。最简单的方法是设计不可变对象(条目 17)。如果对象是不可变的,则失败原子性是必然的。如果一个操作失败,它可能会阻止创建一个新对象,但是它不会让一个现有对象处于不一致的状态,因为每个对象的状态在创建时是一致的,并且在创建后不能修改。
对于对可变对象进行操作的方法,实现失败原子性的最常用方法是:在执行操作之前检查参数的有效性(条目 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),但这种做法并不长用,该代码拦截在操作中发生的失败,并使对象将其状态回滚到操作开始之前的点。 此方法主要用于持久性的(基于磁盘)的数据结构。
虽然失败原子性通常是可取的,但它并不总是可以实现的。例如,如果两个线程试图在没有适当同步的情况下并发地修改同一个对象,那么该对象可能会处于不一致的状态。因此,如果假定在捕捉到ConcurrentModificationException之后对象仍然可用,那就错了。错误是不可恢复的,所以方法在抛出AssertionError时,甚至不需要尝试保存失败原子性。
即使在可能存在实现失败原子性的情况下,也并非总是可取的。 对于某些操作,它会显着增加成本或复杂性。 也就是说,一旦你意识到这个问题,通常都可以自由而轻松地做到失败原子性。
总之,作为规则,任何生成的异常都是方法规范的一部分,应该使对象处于方法调用之前的状态。 违反此规则的地方,API文档应清楚地指出该对象将保留在哪种状态。遗憾的是,许多现有的API文档无法实现这一理想。