Effective Java 3rd 条目17 最小化可变性

一个不可变类简单地讲是实例不可以改变的一个类。在每个实例里面包含的所有信息在对象的生命周期里面是确定的,所以从来不会看到改变。Java平台库包含了许多不可变类,包括String、原始装箱类型和BigInteger与BigDecimal。为此有很多很好的原因:不可变类比可变类更容易设计、实现和使用。它们更不容易出错而且更加安全。

为了使得一个类不可变,遵循五条规则:

  1. 不要提供修改对象状态的方法(被称为设置方法(mutator))。

  2. 确保这个类不会被扩展。这阻止了无意的和恶意的子类,通过好像对象状态已经改变这种方式破解这个类的不可变行为。阻止子类化通常是使得类为final而完成的,但是稍后我们将会讨论的另外一种替代方法。

  3. 使得所有域为final。这清楚地以系统强制的方式表达了你的意图。如果一个新创建对象的引用没有同步地从一个线程被传递到另外一个线程,那么确保正确的行为也是有必要的,就像在内存模型(memory model)中讲清楚的[JLS, 17.5; Goetz06, 16]。

  4. 使得所有域是私有的。这阻止了客户端获取由域引用的可变对象,而且阻止了直接修改这些对象。不可变类有包含原始值或者对不可变对象引用的公开final域,这在技术上是允许的,但是这是不推荐的,因为它阻止了在以后发布中改变内部表示(条目15和16)。

  5. 确保对任何可变组件的排他访问。如果你的类有引用可变对象的任何域,确保这个类的客户端不可能获取这些对象的引用。不要初始化这样的类到一个客户端提供的对象引用,或者从一个访问子返回引用。在构造子、访问子和readObject方法(条目88)上使用防守性拷贝(defensive copy)(条目50)。

在前面条目中的许多例子中的类是不可变的。一个这样的类是条目11中的PhoneNumber,他有为每个属性的访问子,但是没有相应的设置方法。以下是一个稍微复杂的例子:

// 不可变的复数类
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;

        // 参考47页找出为什么使用compare而不是== 
        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)"; 
    }
} 

这个类表示一个复数(一个有实部和虚部的数)。在标准的Object方法之外,它提供了对实部和虚部的访问子,而且提供了基本的算术操作:加法、减法、乘法和除法。注意到算术操作是怎么创建和返回一个新的Complex实例,而不是修改这个实例。这个模式被认为是一个函数式(functional)方式,因为方法返回了应用一个函数到它们操作数的结果而没有修改它。与它形成对比的是过程式(procedural)或者是命令式(imperative)的方式,这个方式里面方法应用一个过程到它们的操作数,造成了它的状态的改变。注意到这个方法名字是介词(比如加上(plus)),而不是动词(比如加(add))。这强调了这个事实:这个方法不会改变对象的值。BigInteger和BigDecimal类没有遵从这个命名惯例,而且这导致了许多使用错误。

如果你不熟悉函数式方式,这种方式可能看起来不自然,但是它使不可变成为可能,这有许多优点。不可变对象是简单的。不可变对象可以一致地在一个状态,也就是创建时的状态。如果你想确保所有构造子建立类不变性,那么保证了这些不变量将会永远保持正确,就你而言或者对于使用这个类的程序员而言,不需要进一步的努力。另外一方面,可变对象可以有随意复杂的状态空间。如果文档没有提供由设置方法执行的状态转换的明确描述,那么可靠地使用一个可变对象是困难的甚至是不可能的。

不可变对象有与生俱来的线程安全性;它们不需要同步。多个线程并行地访问它们也不能损坏它们。这无疑是获得线程安全的最容易的方式。因为没有线程能观察到在一个不可变对象上的另外一个线程的影响,所有可以自由地分享不可变对象。所以不可变类应该鼓励客户端(只要有可能)复用已经存在的实例。这么做一个容易的方式是为通常使用的值提供一个公开静态final的常量。比如,Complex类应该提供这些常量:

扫描二维码关注公众号,回复: 4898553 查看本文章
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);

这个方法可以进一步。一个不可变类可以提供静态工厂(条目1),它缓存经常被请求的实例,现存有实例的时候避免创建新实例。所有的原始装箱类和BigInteger也是这么做的。使用这样的静态工厂造成了客户端分享实例而不是创建新实例,这减少了内存占用和垃圾回收的代价。当设计一个新类时,用静态工厂替代公开构造子,这个优化给你这样的灵活性:以后添加缓冲而不会修改客户端。

不可变对象可以自由地分享这个事实的结果是,你永远不需要对它们的防御性拷贝(条目50)。事实上,你永远根本不需要进行任何拷贝,因为拷贝永远将会和原件相等,所以,你不需要也不应该在不可变类上提供一个克隆方法或者拷贝构造子(条目13)。在Java平台的早期,这没有被很好地理解,所以String方法确实有一个拷贝构造子,但是它应该很少被使用,如果有过的话(条目6)。

不仅你可以分享不可变对象,而且它们可以分享它们的内部构件。比如,BigInteger内部使用一个符号-量值表示法。符号用一个int表示,而量值用一个int队列表示。negate方法产生一个的相同量值和相反符号的新BigInteger。即使它是可变的,它也没必要拷贝队列;新创建的BigInteger和原件一样指向相同的内部队列。

不管可变或者不可变,不可变对象为其他对象准备了很棒的构建模块。如果你知道复杂对象的组件对象不会私下改变,那么维持复杂对象的不可变性更加容易。这个原则的特例是,不可变对象是很好的映射的键和集元素:一旦它们在映射或者集中,你不必要担心它们的值会改变,值改变破坏破坏映射或者集的不可变性。

不可变的对象无偿地提供了失败的原子性(条目76)。它们的状态从来不会改变,所以暂时的不一致也是不可能的。

不可变类的主要缺点在于,对于每个不同的值,它们需要单独的对象。创建这些对象可能代价高,特别它们是很大的时候。比如,假设你有一个百万比特的BigInteger,而且你想改变它的低位比特:

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

flipBit方法创建了一个新的BigInteger,而且长达百万比特,仅仅与原件只有一个比特的不同。这个运算需要正比于BigInteger大小的时间和空间。与这个相反的是java.util.BitSet。就像BigInteger,BitSet代表一个任意长度的比特系列,但是不像BigInteger,BitSet是可变的。BitSet提供了一个方法,允许你以常数时间改变一个百万比特实例的单个比特的状态:

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

如果你执行多个步骤的运算,每一步产生一个新的对象,除了最后的结果最终丢弃所有的对象,那么性能问题被放大。有两种方法处理这个问题。首先,猜测哪个多个步骤的运算通常将被请求,以基元的方式提供它们。如果一个多步骤运算以基元方式提供,不可变类没必要每步都创建一个独立的对象。本质上,不可变类可以任意巧妙。比如,BigInteger有一个包私有的可变“伴随类(companion class)”,它使用于加速多步骤运算,比如模幂运算。相对于使用BigInteger,使用可变伴随类更加困难,就像前面列出的所有原因。幸运的是,你不必要使用它:BigInteger的实现者为你已经做了最困难的工作。

如果你能精确预测客户端想要在你的可变类上执行哪些复杂操作,那么包私有可变伴随类方式工作良好。如果不是,那么你最好的选择是提供一个公开的可变伴随类。这个方式在Java平台库中的主要例子是String类,它的可变伴随是StringBuilder(和它的废弃前身,StringBuffer)。

既然你知道了如果构建一个不可变类,而且你懂得不可变的优缺点,那么让我们讨论一些设计的替代方案。回忆到,为了保证不变性,一个类一定不能允许自己被子类化。这可以通过让这个类为final完成,但是有另外一个更加灵活的替代方案。不是让一个不可变类是final,你可以让它的所有构造子是私有的或者包私有的,而且添加公开静态工厂代替公开构造子(条目1)。为了让这个更加具体,如果你采用这个方法,下面是Complex的看上去的样子:

// 有静态工厂而不是构造子的不可变类。
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); 
    }
    ... // 其余省略
} 

这个方式通常是最好的替代方案。它是最灵活的,因为它允许多个包私有实现类的使用。对于属于包外的它的客户端,不可变类是有效的final,因为一个类来自于另外一个包而且缺少公开或者受保护构造子,扩展它是不可能的。除了允许多个实现类的灵活性,而且通过增强静态工厂的对象缓冲能力,这个方法使得在后续的发布中对这个类进行性能调优是可能的。

当编写BigInteger和BigDecimal时,不可变类不得不是有效的final,这在当初没有被广泛地理解,所以可以覆写它们的所有方法。不幸的是,为了保持向后兼容性这个事实,这不可能被改正。如果你编写一个类,它的安全性取决于BigInteger或者BigDecimal参数的不可变性,这参数来自不可信的客户端,那么你必须检查看一下这个参数是否是一个“真的”BigInteger或者BigDecimal,而不是不信任子类的一个实例。如果它是后者,基于假设它可能是可变的,你必须进行防御性拷贝(条目50):

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

在这个条目的开始的时候,不可变类的规则列表如是说,没有方法可以修改这个对象,而且它的所有域可以在对象状态上产生一个外部可见性(externally visible)改变。事实上,这些规则比必需的更加强化了些,而且为了改进性能可以放松。事实上,没有方法可以在对象状态上产生一个外部可见性变化。然而,一些不变类有一个或者多个非final域,这个域中第一次需要它们时候缓存了它们代价高的计算结果。如果再次请求同一个值,那么返回缓存值,这避免了重复计算的代价。这技巧起作用,因为这个对象是不可变的,这保证了如果计算是重复的,计算产生相同的结果。

比如,PhoneNumber的hashCode方法(条目11),第一次被调用时计算哈希码,缓存它以备它再次被调用。这个技巧,一个懒加载(lazy initialization)(条目83)的例子,也被使用在String上。

应该添加关于系列化的一个告诫。如果你选择让你的不可变类实现Serializable,而且它包含了引用可变对象的一个或者多个域,那么你必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared方法,即使默认的系列化形式是可以接受的。否则,一个攻击者可以创建你的类的一个可变实例。这个主题在条目88中详细讨论。

概括地讲,忍住为每个获取方法编写一个设置方法的冲动。类应该是不可变的,除非有很好的理由让它们是可变的。不可变类提供了许多优点,而且它们的唯一缺点是,在特定情形下有性能问题的可能。你应该总是让小值对象(比如PhoneNumber和Complex)是不可变的。(在Java平台库中有许多类,比如java.util.Date和java.awt.Point,应该是不可变的,但是它们并不是。)你应该认真考虑使得更大的值对象(比如String和BigInteger)也是不可变的。仅仅当你确信取得令人满意的性能是必需的(条目67)时,你应该为你的不可变类提供一个公开可变伴随类。

有些类,为它实施不可变性是不切实际的。如果一个类不可能被创建为不可变的,尽量限制他的可变性。减少一个类可以存在的状态数量,使得思索这个类更加容易,而且减少了错误的可能性。所以,让每个域是final的,除非有迫不得已的理由使得它是非final的。结合这个条目的建议和条目15的建议,你的自然倾向应该是声明每个域为私有final的,除非有很好的理由不这么做

构造子应该使用它们已经确定的约束关系创建充分初始化的对象。除了构造子或者静态方法,不要单独地提供一个公开的初始化方法,除非有迫不得已的理由这么做。相似地,不要提供一个“重新初始化”的方法,使得一个类可以重新使用,就像它是使用不同的初始化状态构建的。这样的方法通常以增加复杂性为代价提供了很少的(如果有)性能优势。

CountDownLatch类例证了这些原则。它是可变的,但是它的状态空间有意地维持很小。你创建了一个对象,使用它一次,然后结束了:倒计数锁存的基数达到了0,你不应该重复利用它。

应该添加的最后的注意事项,是关于这个条目里面的Complex类。这个例子用意仅仅在于说明不可变性。这不是一个工业强度的复数实现。为复数的乘法和除法,它使用了标准的公式,而没有正确地四舍五入,而且对于复数的NaN和无穷[Kahan91, Smith62, Thomas94]提供了糟糕的语义。

猜你喜欢

转载自blog.csdn.net/tigershin/article/details/79121454
今日推荐