ITEM 17:最小化可变性

ITEM 17: MINIMIZE MUTABILITY
  不可变类就是一个实例不能被修改的类。每个实例中包含的所有信息在对象的生命周期中都是固定的,因此不能观察到任何更改。Java平台库包含许多不可变类,包括String、装箱的原语类以及 BigInteger 和 BigDecimal。这样做有很多好的理由:不可变类比可变类更容易设计、实现和使用。它们不太容易出错,而且更安全。要使类不可变,请遵循以下五条规则:

  1. 不要提供修改对象状态的方法(称为mutators)。
  2. 确保类不能被扩展。这可以防止粗心或恶意的子类破坏类的不可变行为,表现得好像对象的状态已经更改。防止子类化通常通过 final 来实现,但是我们稍后将讨论另一种方法。
  3. 使所有字段为final。这以系统强制执行的方式清楚地表达了您的意图。此外,这也确保了将实例引用从一个线程传递到另一个线程在没有同步的情况下行为正确,如内存模型。
  4. 所有字段置为 private。这将阻止用户访问字段引用的可变对象并直接修改这些对象。虽然在技术上允许不可变类拥有包含原始值或对不可变对象的引用的公共 final 字段,但不建议这样做,因为它排除了在稍后版本中更改内部表示(item 16、17)。
  5. 确保对任何可变组件的独占访问。如果您的类有引用可变对象的任何字段,请确保该类的用户不能获得对这些对象的引用。永远不要将这样的字段初始化为向用户提供的对象引用,也不要从访问器返回该字段。在构造函数、访问器和 readObject 方法中创建防御性副本(item 90)。
      前面项目中的许多示例类都是不可变的。item 11 中的 PhoneNumber 就是这样一个类,它有每个属性的访问器,但是没有相应的变量。下面是一个稍微复杂一点的例子:
// Immutable complex number class 
public final class Complex {
  private final double re; 
  private final double im;
  public Complex(double re, double im) { 
    this.re = re;
    this.im = im;
  }
  public double realPart() { return re; } 
  public double imaginaryPart() { return im; }
  public Complex plus(Complex c) {
    return new Complex(re + c.re, im + c.im);
  }
  public Complex minus(Complex c) { 
    return new Complex(re - c.re, im - c.im);
  }
  public Complex times(Complex c) {
    return new Complex(re * c.re - im * c.im, re * c.im + im * c.re); 
  }
  public Complex dividedBy(Complex c) {
    double tmp = c.re * c.re + c.im * c.im;
    return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
  }
  @Override public boolean equals(Object o) { 
    if (o == this)
      return true;
    if (!(o instanceof Complex))
      return false;
    Complex c = (Complex) o;
    // See page 47 to find out why we use compare instead of == return 
    Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0; 
  }
  @Override public int hashCode() {
    return 31 * Double.hashCode(re) + Double.hashCode(im);
  }
  @Override public String toString() { 
    return "(" + re + " + " + im + "i)";
  } 
}

  这个类表示一个复数(包含实部和虚部的数)。除了标准的对象方法之外,它还提供实部和虚部的访问器,并提供四种基本的算术操作:加法、减法、乘法和除法。注意算术运算如何创建和返回一个新的复杂实例,而不是修改这个实例。这种模式称为函数方法,因为方法返回将函数应用于其操作数的结果,而不修改它。将其与程序性或命令性方法进行比较,在程序性或命令性方法中,方法将过程应用于其操作数,从而导致其状态发生更改。注意,方法名是介词(如plus),而不是动词(如add)。这强调了方法不会改变对象的值。BigInteger 和 BigDecimal 类不遵守这种命名约定,这导致了许多使用错误。
  如果您不熟悉函数方法,那么它可能看起来不自然,但是它使不变性成为可能,这有许多优点。不可变对象很简单。不可变对象可以恰好处于一种状态,即创建它的状态。如果您确保所有构造函数都建立了类不变量,那么就可以保证这些不变量始终为真,而不需要您或使用该类的程序员做更多的工作。另一方面,可变对象可以具有任意复杂的状态空间。如果文档没有提供由mutator方法执行的状态转换的精确描述,则很难或不可能可靠地使用可变类。
  不可变对象本质上是线程安全的;它们不需要同步。它们不会被并发访问它们的多个线程破坏。这无疑是实现线程安全的最简单方法。由于没有线程能够观察到另一个线程对不可变对象的任何影响,所以可以自由地共享不可变对象。因此,不可变类应该鼓励用户尽可能重用现有实例。一个简单的方法是为常用值提供公共静态最终常量。例如,复杂类可能提供以下常量:

public static final Complex ZERO = new Complex(0, 0); 
public static final Complex ONE = new Complex(1, 0); 
public static final Complex I = new Complex(0, 1);

  这种方法可以更进一步。不可变类可以提供静态工厂(item 1),缓存频繁请求的实例,以避免在现有实例需要时创建新实例。所有装箱的原语类和 BigInteger 都这样做。使用这样的静态工厂会导致客户机共享实例,而不是创建新的实例,从而减少内存占用和垃圾收集成本。在设计一个新类时,选择静态工厂而不是公共构造函数,可以在不修改客户机的情况下灵活地添加缓存。
  不可变对象可以自由共享这一事实的一个结果是,您永远不必创建它们的防御性副本(item 50)。事实上,你根本不需要复制任何文件,因为这些文件将永远与原件等价。因此,您不需要也不应该在不可变类上提供 clone method 或复制构造函数(item 13)。在Java平台的早期,这一点并没有得到很好的理解,所以 String 类确实有一个复制构造函数,但是它应该很少被使用(item 6)。
  您不仅可以共享不可变对象,而且可以共享它们的内部。例如,BigInteger 类在内部使用符号数值表示法。符号由int表示,数值由int数组表示。
  negate 方法产生一个新的大小相同的大整数,符号相反。它不需要复制数组,即使它是可变的;新创建的 BigInteger 指向与原始数组相同的内部数组。
  不可变对象为其他对象(无论是可变的还是不可变的)提供了很好的构建块。如果知道复杂对象的组件对象不会在其下发生变化,那么维护复杂对象的不变量就容易得多。这个原则的一个特殊情况是,不可变对象可以作为map键和set元素:一旦它们被放入 map 或者 set,即使map 或者 set被修改,它们的值也不会发生变化。
  不可变对象天然提供原子性(item 76)。它们的状态永远不会改变,因此不可能出现暂时的不一致。
  不可变类的主要缺点是,对于每个不同的值,它们都需要一个单独的对象。创建这些对象的成本可能很高,尤其是当它们很大的时候。例如,假设您有一个百万比特的BigInteger,您想改变它的低阶位:

BigInteger moby = ...; 
moby = moby.flipBit(0);

  flipBit 方法创建了一个新的 BigInteger 实例,也有一百万比特长,它与原来的BigInteger 实例只差一个比特。与 java.util.BitSet 相比,BigInteger 的操作需要与其 大小成比例的时间和空间。
  与 BigInteger 一样,BitSet 表示任意长的位序列,但与 BigInteger 不同,BitSet 是可变的。BitSet 类提供了一种方法,可以让你在恒定的时间内改变上百万位实例的单个位的状态:

BitSet moby = ...; 
moby.flip(0);

  如果您执行一个多步骤操作,在每一步生成一个新对象,最终丢弃除最终结果之外的所有对象,那么性能问题将被放大。有两种方法来处理这个问题。首先是猜测通常需要哪些多步骤操作,并将它们作为基本类型提供。如果将多步骤操作作为基元提供,则不可变类不必在每个步骤中创建单独的对象。在内部,不可变类可以是任意聪明的。例如,BigInteger 有一个包私有的可变“伴侣类”,它用于加速多步操作,比如模幂运算。由于前面列出的所有原因,使用可变伴侣类要比使用 BigInteger 困难得多。幸运的是,您不必使用它:BigInteger 的实现者为您做了艰苦的工作。
  如果您能够准确地预测哪些复杂的操作客户机将希望在您的不可变类上执行,那么package-private 可变伴侣类方法就可以很好地工作。如果没有,那么您最好提供一个公共可变伴侣类。这种方法在 Java 平台库中的主要例子是 String 类,它的可变伙伴是 StringBuilder (及其过时的前辈 StringBuffer)。
  既然您已经知道了如何创建一个不可变的类,并且了解了不可变的优缺点,那么让我们来讨论一些设计替代方案。记住,为了保证不变性,类必须不允许自己被子类化。这可以通过 final 来实现,但是还有另一个更灵活的选择。您可以将它的所有构造函数都设置为 private 或 package-private,并添加公共静态工厂来代替公共构造函数(item 1),而不是创建一个不可变的 final 类。

// Immutable class with static factories instead of constructors
public class Complex {
  private final double re; 
  private final double im;
  private Complex(double re, double im) { 
    this.re = re;
    this.im = im;
  }
  public static Complex valueOf(double re, double im) { 
    return new Complex(re, im);
  }
  ... // Remainder unchanged 
}

  这种方法通常是最好的选择。它是最灵活的,因为它允许使用多个包私有实现类。对于驻留在包外部的客户端,不可变类实际上是 final,因为不可能扩展来自另一个包且缺少公共或受保护构造函数的类。除了允许多个实现类的灵活性之外,这种方法还可以通过改进静态工厂的对象缓存功能来调优后续版本中该类的性能。
  当编写 BigInteger 和 BigDecimal 时,不可变类必须是有效的 final,因此它们的所有方法都可能被覆盖,这一点没有得到广泛的理解。不幸的是,在保留向后兼容性的同时,无法在事后纠正此错误。如果您编写的类的安全性依赖于来自不可信客户机的BigInteger 或 BigDecimal 参数的不可变性,则必须检查该参数是否是“真正的” BigInteger 或 BigDecimal,而不是不可信子类的实例。如果是后者,你必须防御性地复制它,假设它可能是可变的(item 50):

public static BigInteger safeInstance(BigInteger val) { 
  return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray()); }

  该项目开头的不可变类规则列表表示,任何方法都不能修改对象,且其所有字段必须为 final。事实上,这些规则比必要的更强一些,可以通过放松来提高性能。实际上,任何方法都不能在对象状态中产生外部可见的更改。然而,一些不可变类有一个或多个非 final字段,在第一次需要它们时,它们在其中缓存昂贵计算的结果。如果再次请求相同的值,则返回缓存的值,从而节省重新计算的成本。这种技巧之所以奏效,正是因为对象是不可变的,这就保证了如果重复计算,计算结果将是相同的。
  例如,PhoneNumber 的 hashCode 方法(item 11)在第一次调用时计算散列代码,并在再次调用时缓存它。这个技术是一个延迟初始化的例子(item 83),String也使用它。
  关于可序列化,应该添加一个警告。如果您选择让您的不可变类实现 Serializable,并且它包含一个或多个引用可变对象的字段,那么您必须提供一个显式的 readObject或 readResolve 方法,或者使用 ObjectOutputStream.writeUnshared 和 ObjectInputStream.readUnshared 方法,即使默认的序列化形式是可接受的。否则,攻击者可能创建类的可变实例。item 88 详细讨论了这个主题。
  总之,不要急于为每个 getter 编写 setter。类应该是不可变的,除非有很好的理由使它们是可变的。不可变类提供了许多优点,它们唯一的缺点是在某些情况下可能出现性能问题。您应该始终使小的值对象,如 PhoneNumber 和 Complex,不可变(Java平台库中有几个类,比如 java.util.Date 和 java.awt.Point,它应该是不可变的,但它们不是。)。您应该认真考虑将更大的值对象(如 String和 BigInteger)也设置为不可变的。只有在您确认有必要获得令人满意的性能之后,您才应该为您的不可变类提供一个公共可变伴侣类(item 67)。
  有些类的不变性是不切实际的。如果一个类不能变成不可变的,那么就尽可能地限制它的可变性。减少一个对象可能存在的状态数,可以更容易地对该对象进行推理,并降低出错的可能性。因此,除非有令人信服的理由,否则将每个字段设置为 final。把这一项的建议和item 15 的建议结合起来,你的自然倾向应该是:除非有充分的理由,否则不要公开“私人领域”。
  构造函数应该创建完全初始化的对象,并建立它们的所有不变量。除非有必要,否则不要提供与构造函数或静态工厂分离的公共初始化方法。类似地,不要提供 “reinitialize” 方法,该方法允许重用对象,就像使用不同的初始状态构造对象一样。这样的方法通常提供很少的性能好处,但增加了复杂性。
  CountDownLatch类例证了这些原则。它是可变的,但是它的状态空间故意保持得很小。您创建一个实例,使用它一次,它就完成了:一旦倒计时锁的计数达到零,您就不能重用它。
  应在本项目中添加关于复杂类的最后说明。这个例子只是为了说明不变性。它不是工业级的复数实现。它使用了复杂乘法和除法的标准公式,这些公式没有正确四舍五入,并且为复杂的 NaNs 和无穷大提供了糟糕的语义。

你可能感兴趣的:(ITEM 17:最小化可变性)