effective-java-3rd-chinese/docs/notes/76. 保持失败原子性.md
mr-dragon 68acbcf2e1 在每篇文章后面加2个换行
在每篇文章后面加2个换行,方便使用cat命令合并成一个文件的时候不会影响目录的生成
2019-06-14 13:56:01 +08:00

33 lines
4.5 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 76. 保持失败原子性
  当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中, 即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者期望能从这种异常中进行恢复。**一般而言,失败的方法调用应该使对象保持在被调用之前的状态。** 具有这种属性的方法被称为具有失败原子性 failure atomic
  有几种途径可以实现这种效果。最简单的办法莫过于设计一个不可变的对象 (详见第 17 条) 。如果对象是不可变的,失败原子性就是显然的。如果一个操作失败了,它可能会阻止创建新的对象,但是永远也不会使已有的对象保持在不一致的状态之中,因为当每个对象被创建之后它就处于一致的状态之中,以后也不会再发生变化。
  对于在可变对象上执行操作的方法,获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性 (详见第 49 条)。这可以使得在对象的状态被修改之前,先抛出适当的异常。比如,以第 7 条中的 Stack.pop 方法为例:
```java
public Object pop() {
if ( size == 0 )
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; /* Eliminate obsolete reference */
return(result);
}
```
  如果取消对初始大小size的检查当这个方法企图从一个空栈中弹出元素时它仍然会抛出异常。然而这将会导致 size 字段保持在不一致的状态(负数)之中,从而导致将来对该对象的任何方法调用都会失败。此外,那时, pop 方法抛出的 `ArrayIndexOutOfBoundsException` 异常对于该抽象来说也是不恰当的(详见第 73 条)。
  一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。如果对参数的检查只有在执行了部分计算之后才能进行,这种办法实际上就是上一种办法的自然扩展。比如,以 `TreeMap` 的情形为例,它的元素被按照某种特定的顺序做了排序。为了向 `TreeMap` 中添加元素,该元素的类型就必须是可以利用 `TreeMap` 的排序准则与其他元素进行比较的。如果企图增加类型不正确的元素,在 tree 以任何方式被修改之前,自然会导致 `ClassCastException` 异常。
  第三种获得失败原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。如果数据保存在临时的数据结构中,计算过程会更加迅速,使用这种办法就是件很自然的事。例如,有些排序函数会在执行排序之前,先把它的输入列表备份到一个数组中,以便降低在排序的内循环中访问元素所需要的开销。这是出于性能考虑的做法,但是,它增加了一项优势:即使排序失败,它也能保证输入列表保持原样。
  最后一种获得失败原子性的办法远远没有那么常用,做法是编写一段恢复代码 recovery code由它来拦截操作过程中发生的失败以及便对象回滚到操作开始之前的状态上。这种办法主要用于永久性的基于磁盘的 数据结构。
  虽然一般情况下都希望实现失败原子性,但并非总是可以做到。举个例子,如果两个线程企图在没有适当的同步机制的情况下,并发地修改同一个对象,这个对象就有可能被留在不一致的状态之中。因此,在捕获了 `ConcurrentModificationException` 异常之后再假设对象仍然是可用的,这就是不正确的。错误通常是不可恢复的,因此,当方法抛出 `AssertionError` 时,不需要努力去保持失败原子性。
  即使在可以实现失败原子性的场合,它也并不总是人们所期望的。对于某些操作,它会显著地增加开销或者复杂性。也就是说, 一旦了解了这个问题,获得失败原子性往往既简单又容易。
  总而言之,作为方法规范的一部分,它产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API 文档就应该清楚地指明对象将会处于什么样的状态。遗憾的是,大量现有的 API 文档都未能做到这一点。