【Effective Java】Ch4_Class:Item15_使可变性最小化

 Item 15: Minimize mutability

不可变类是指其实例不可被修改的类。实例中的所有信息都是在创建实例时提供的,并且在对象生命周期内保持不变。JDK中有许多这种不可变类,例如String、基本类型包装类、BigInteger、BigDecimal。

不可变类有许多优点:易于设计、易于实现、易于使用。它们更不容易出错,并且更安全。

如何编写不可变类

要让类变成不可变,要遵循以下五条原则:

  1. 不提供任何修改类状态的方法(mutator);
  2. 保证类不能被扩展。这可以防止粗心或者恶意的子类破坏不可变行为。可以将类声明为final,后面我们也会讨论其他方法;
  3. 使所有域都是final的。通过系统强制的方式明确地表明你的意图。Also, it is necessary to ensure correct behavior if a reference to a newly created instance is passed from one thread to another without
    synchronization, as spelled out in the memory model [JLS, 17.5; Goetz06 16].
  4. 使所有域都是private的。这可以防止可变类引用这些域,并直接修改。虽然从技术上讲允许不可变类具有public final域,只要这些域指向基本类型或者不可变对象,但是不建议这样做,因为这样会导致以后无法改变内部表示(Item13)。
  5. 确保对任何可变组件的访问都是互斥的
    如果类中有指向可变对象的域,那就需要确保客户端不能获得这些可变对象的引用。
    永远不要将这种域初始化为客户端提供的对象引用。
    不要通过accessor返回这种对象引用。
    在构造函数、accessor、readObject方法(Item76)中使用保护性拷贝(Item39)。
之前Item中的许多例子都是不可变类,例如Item9中的PhoneNumber,每个属性都有accessor,但是没有mutator。如下是一个更复杂一些的例子:
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
  this.re = re;
  this.im = im;
}
// Accessors with no corresponding mutators
public double realPart()      { return re; }
public double imaginaryPart() { return im; }

public Complex add(Complex c) {
  return new Complex(re + c.re, im + c.im);
}
public Complex subtract(Complex c) {
  return new Complex(re - c.re, im - c.im);
}
public Complex multiply(Complex c) {
  return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
}
public Complex divide(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 43 to find out why we use compare instead of == //因为有Double.NaN
  return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
}
@Override
public int hashCode() {
  int result = 17 + hashDouble(re);
  result = 31 * result + hashDouble(im);
  return result;
}
private int hashDouble(double val) {
  long longBits = Double.doubleToLongBits(re);
  return (int) (longBits ^ (longBits >>> 32));
}
@Override
public String toString() {
  return "(" + re + " + " + im + "i)";
}
 

该类表示一个复数,分别为实部和虚部提供了accessor,还提供了4中基本算术运算:加减乘除。注意这些算术运算创建并返回一个新的Complex实例,而不是修改当前实例。大所数重要的不可变类都是用了这种模式。这被称为函数方式functional approach),因为这些方法返回对操作数的运算结果,但是并不修改这些操作数。与之对应的则是更常见的过程方式(procedural approach),或称为命令方式imperative approach),会改变操作数状态。

优点

如果你对函数方式不熟悉,可能会觉得它不太自然,但是它带来了不可变性,具有许多优点。

  • 简单

不可变对象比较简单。不可变对象只有一种状态,就是创建时的状态。只要保证所有的构造方法都建立了类的约束关系,那么这些约束关系在任何时候都保持不变,使用该类的程序员无需再做任何额外工作。而另一方面,可变对象则可以有任意的复杂的状态空间。如果文档没有对mutator方法执行的状态转换提供准确的描述,则很难甚至不可能可靠地使用一个可变类。

  • 线程安全,可自由地共享

不可变对象生来就是线程安全的,他们不需要同步。当多个线程并发访问不可变对象时,他们不会遭到破坏。这无疑是实现线程安全的最容易的方法。实际上,不会有线程能观察到其他线程对不可变对象的影响。所以不可变对象可以被自由地共享。不可变类应当利用这种优势,鼓励客户端尽可能重用现有实例。一个简单的方法是为常用的值提供public static final的常量。例如Complex类可以提供这些常量:

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);
 
    public static final BigInteger ZERO = new BigInteger(new int[0], 0);
    public static final BigInteger ONE = valueOf(1);
    public static final BigInteger TEN = valueOf(10);

这种方法可以进一步扩展,不可变对象可以提供静态工厂(Item1),将经常被请求的实例缓存起来,当现有实例满足请求的时候,就不必去创建新的实例。【例】所有的基本类型包装类、BigInteger都是这样做的。使用静态工厂使得客户端共享实例而不是创建新实例,可以降低内存占用和垃圾收集成本。设计新类时选择用静态工厂而不是公共构造方法,可以让你以后灵活地添加缓存,而不必修改客户端。

  • 无需保护性拷贝,无需提供拷贝构造方法

不可变对象可以自由共享导致的一个后果是,你无需进行保护性拷贝了(Item39)。实际上你根本无需做任何拷贝,因为这些拷贝始终与源对象相等。因此,你不需要,也不应该为不可变类提供clone方法或者拷贝构造函数Item11)。【反例】这一点在Java平台早期并没有被很好地理解,导致String类具有拷贝构造函数,应该尽量不去用这个函数(Item5)。

  /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
    }
 
  • 可以共享内部信息

不仅可以共享不可变对象,还可以共享他们的内部信息。【例】BigInteger类内部使用了一个符号数值表示法(sign-magnitude representation),符号用一个int表示,数值则用一个int数组表示。negate()方法会创建一个数值相同但符号相反的新BigInteger,该方法不需要拷贝数组,新创建的BigInteger只需要指向源对象中的数组即可。
    /**
     * Returns a BigInteger whose value is {@code (-this)}.
     *
     * @return {@code -this}
     */
    public BigInteger negate() {
          return new BigInteger(this.mag, -this.signum);
    }
  • 为其他对象提供构件

不可变对象为其他可变或不可变对象提供大量的构件。如果你知道一个复杂对象内部的组件对象是不可变的,那么维护他的约束关系就更容易。这条原则的一个例子是,不可变对象构成了大量的map key和set元素,一旦不可变对象进入map或set中,你就不必担心他们的值变化导致破坏map和set的约束关系。

缺点

  • 对于每个不同的值都需要一个单独的对象

不可变类的唯一缺点是,对于每个不同的值都需要一个单独的对象。而创建这些对象的成本可能很高,尤其当对象很大时。

【例】例如假设你有一个上百万bit的BigInteger,然后你需要改变他的低位bit:

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

flipBit()方法会创建一个新的BigInteger实例,同样有上百万位长,仅仅与源对象有一个bit不同。这个操作所消耗的时间和空间与BigInteger的大小成正比。与java.util.BitSet进行比较,BitSet也代表任意长度的bit序列;但与BigInteger不同,BitSet是可变的。BigSet提供叻一个方法,允许在固定时间内改变百万bit实例中的某一个bit的状态。

 

如果执行多步操作时每一步都创建一个新对象,最终只保留最终的结果对象,而丢弃其他对象,则性能问题就会被放大。有两个方法解决这个问题。

方法一,猜测哪些多步操作经常使用,然后将他们作为基本类型提供。这样不可变类就无需每一步创建一个单独对象。不可变类内部更加clever。【例】例如BigInteger有一个包级私有的可变的“companion class”,用于加快例如模幂运算(modular exponentiation)这样的多步运算的速度。使用可变的companion class要比使用BigInteger复杂地多,但幸运的是你不必这样做,因为BigInteger替你完成叻困难的工作。——?

如果你能准确预测客户端希望在你的不可变类上执行哪些复杂的多步操作,那么方法一就可以工作地很好。

方法二,提供一个public的可变companion class。【例】JDK中有一个例子就是String类,其可变companion是StringBuilder,以及基本上废弃的StringBuffer。在特定情况下,BitSet扮演着BigInteger的可变companion角色。

如何禁止子类化

  • 用静态工厂替代public构造函数

现在你知道了如何构建不可变类,并且了解了其优点和缺点,接下来我们讨论一些其他的可替代方案。前面说过,为了保证不可变性,类不应该被子类化。通常可以让类为final,但是还有其他的更加灵活的方法。其中替代方案是让不可变类的所有构造方法private或者package-private,并添加public static的工厂类替代public构造函数(Item1)。

// 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的,因为不可能扩展另一个包中的没有public或protected构造函数的类。除了元素多个实现类这种灵活性之外,这种方法还使得可能通过提高静态工厂的缓存能力,从而在后续版本中改进该类的性能。

静态工厂与构造函数相比还具有许多其他优势(Item1)。例如假设你希望提供一种基于极坐标创建复数的方法,如果用构造函数则会很混乱,因为想要的构造函数与Complex(double, double)具有相同的方法签名。而用静态工厂则很简单,只需要增加第二个静态工厂,且工厂的名字清楚地表明它的功能:

public static Complex valueOfPolar(double r, double theta) {
  return new Complex(r * Math.cos(theta),
                     r * Math.sin(theta));
}
  • bug:BigInteger和BigDecimal实际上可子类化

当BigInteger和BigDecimal编写出来时,对于“不可变类实际上必须final”并没有得到广泛的理解,所以这两个类的方法都可以被重写。不幸的是,为了保持向后兼容,这个问题一直没有得到修正。如果你编写的类的安全性依赖于(来自不可信客户端的)BigInteger或BigDecimal的不可变性,那么就必须检查参数是真正的BigInteger/BigDecimal,还是不可信任的子类实例。如果是后者,你必须把它当成是可变的,并进行保护性拷贝(Item39):

public static BigInteger safeInstance(BigInteger val) {
  if (val.getClass() != BigInteger.class)
         return new BigInteger(val.toByteArray());
 return val;

不可变类可以有nonfinal域,用于存储缓存

本章开头提到的构造不可变类的规则中指出,没有方法能修改对象,并且它的所有域必须是final的。实际上这些规则比较强硬,为了提供性能可以有所放松。实际上应该是没有方法能够对类的状态产生外部可见的改变(no method may produce an externally visible change in the object’s state)。然而,一些不可变类拥有一个或多个nonfinal域,用于缓存昂贵计算的结果,如果再次请求相同的计算,则直接返回缓存值,以解决重新计算的成本。这个技巧可以很好地工作,因为对象是不可变的,保证了相同的计算总是返回同样的结果。

【例】例如PhoneNumber类的hashCode方法(Item9),在第一次调用时计算哈希值并缓存起来,以备再次调用的时候使用。这种技术也是延迟初始化的一个例子,String类也用到了。

    /** Cache the hash code for the string */
    private int hash// Default to 0
 
    public int hashCode() {
      int h = hash;
      int len = count;
      if (h == 0 && len > 0) {
        int off = offset;
        char val[] = value;
            for (int i = 0; i < len; i++) {
                h = 31*h + val[off++];
            }
            hash = h;
      }
      return h;
    }

不可变类的序列化

关于序列化需要注意一点:如果你选择让你的不可变类实现Serializable ,并且拥有一个或多个域指向可变对象,那么你就需要显式地提供readObjectreadResolve方法,或者用 ObjectOutputStream.writeUnsharedObjectInputStream.readUnshared 方法,即便默认的序列化形式是可以接受的也是如此。否则攻击者可以创建一个你的不那么不可变类的一个可变实例(Otherwise an attacker could create a mutable instance of your not-quite-immutable class)(Item76)。

总结

总之,不要为每个get方法写一个set方法除非有很好的理由要让类可变,否则都应该让类不可变。不可变类有许多优点,唯一的缺点是可能在特定情况下存在潜在的性能问题。你应该永远把小的值对象设计成不可变,例如PhoneNumber、Complex。【反例】Java平台库中有许多这种类,例如java.util.Date、java.awt.Point,它们理论上应当是不可变的,但实际上却是可变的。

你也应该认真考虑把较大的值对象设计成不可变,例如String、BigInteger。如果你确认有必要实现令人满意的性能(Item55),那么就应当提供一个public的可变companion class(配套类)。

对于某些类来说,设计成不可变是不切实际的。如果一个类不能设计为不可变,那就尽可能地限制它的可变性。降低对象可以存在的状态数(——变量数目),可以减少出错的可能。因此,除非有令人信服的理由让使域变成nonfinal,否则都应该使每个域都是final的。 Therefore, make every field final unless there is a compelling reason to make it nonfinal.

构造函数应该创建完全初始化的对象,并建立起所有的约束关系。不要独立于构造函数或静态工厂之外再提供一个public的初始化方法,除非有令人信服的理由必须这么做。同样,不要提供“重新初始化”方法,想要使得对象是由不同的初始化状态构造的一样。与其增加的复杂性相比,这种方法带来的性能提升是很小的。

【例】TimerTask类例证了这个原则,该类是可变的,但是他的状态空间控制得非常小。你可以创建一个实例,调度它来执行,随意地取消它。一旦task运行完毕,或者被取消,你就不可以再次调度它了。

最后一点要注意的是与本节中的Complex类有关。该类仅仅是用来演示不可变形,并不是一个产品级的复数实现,它用标准的计算公式来实现复数的乘法和除法,会进行不正确的舍入,对复数NaN和无穷大没有提供很好的语义。

 

 

 

 

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读