ITEM 76: STRIVE FOR FAILURE ATOMICITY
即使在一个对象在执行操作的过程中发生了故障,抛出异常之后,通常也希望该对象仍然处于定义良好的可用状态。对于受控异常尤其如此,调用者需要从中恢复。一般来说,失败的方法调用应该使对象保持在调用之前的状态。具有此属性的方法称为故障原子性方法( failure-atomic)。
有几种方法可以实现这种效果。最简单的是:设计不可变对象(item 17)。如果一个对象是不可变的,实现故障原子性方法是毫无成本的。如果一个操作失败,它可能会阻止创建新对象,但它不会让现有对象处于不一致的状态,因为每个对象的状态在创建时是一致的,此后无法修改。
对于操作可变对象的方法,实现故障原子性的最常见方法是在执行操作之前检查参数的有效性(item 49)。这将导致在对象修改开始之前抛出大多数异常。例如 item 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 也不适合抽象(item 73)。
与实现故障原子性密切相关的一种方法是对计算进行排序,以便可能失败的部分在修改对象的部分之前发生。当不执行部分计算就不能检查参数时,这种方法是前一种方法的自然扩展。例如,考虑 TreeMap 的情况,它的元素是按照某种顺序排序的。为了向TreeMap添加元素,元素的类型必须能够使用 TreeMap 的排序进行比较。在以任何方式修改树之前,尝试添加类型不正确的元素自然会失败,并会出现 ClassCastException,这是在树中搜索元素的结果。
实现故障原子性的第三种方法是对对象的临时副本执行操作,并在操作完成后用临时副本替换对象的内容。一旦数据存储在临时数据结构中,就可以更快地执行计算,这种方法自然就会出现。例如,一些排序函数在排序之前将其输入列表复制到数组中,以减少在排序的内部循环中访问元素的开销。这样做是为了提高性能,但是作为额外的好处,它确保了在排序失败时不会改变输入列表。
实现故障原子性的最后一种方法是编写恢复代码,拦截在操作过程中发生的故障,并导致对象将其状态回滚到操作开始前的状态。这种方法主要用于持久的(基于磁盘的)数据结构。
虽然故障原子性通常是可取的,但它并不总是可以实现的。例如,如果两个线程试图在没有适当同步的情况下并发地修改同一个对象,该对象可能会处于不一致的状态。因此,假设一个对象在捕获了 ConcurrentModificationException 之后仍然可用是错误的。错误是不可恢复的,因此在抛出 AssertionError 时,您甚至不需要试图保持故障原子性。
即使在可能存在故障原子性的地方,也不总是可取的。对于某些操作而言,它将显著增加成本或复杂性。也就是说,一旦您意识到问题的存在,通常可以轻松地实现故障原子性。
总之,作为规则,作为方法规范一部分的任何生成的异常都应该使对象保持在方法调用之前的相同状态。在违反此规则的地方,API文档应该清楚地指出对象将处于什么状态。不幸的是,现有的大量API文档都不能满足这一理想。