Effective Java 第三版——17. 最小化可变性

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。java

Effective Java, Third Edition

17. 最小化可变性

不可变类简单来讲是它的实例不能被修改的类。 包含在每一个实例中的全部信息在对象的生命周期中是固定的,所以不会观察到任何变化。 Java平台类库包含许多不可变的类,包括String类,基本类型包装类以及BigInteger类和BigDecimal类。 有不少很好的理由:不可变类比可变类更容易设计,实现和使用。 他们不太容易出错,更安全。程序员

要使一个类不可变,请遵循如下五条规则:数组

  1. 不要提供修改对象状态的方法(也称为mutators)。
  2. 确保这个类不能被继承。 这能够防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变行为。 防止子类化一般是经过final修饰类,可是咱们稍后将讨论另外一种方法。
  3. 把全部属性设置为final。经过系统强制执行,清楚地表达了你的意图。 另外,若是一个新建立的实例的引用从一个线程传递到另外一个线程而没有同步,就必须保证正确的行为,正如内存模型[JLS,17.5; Goetz06,16]所述。
  4. 把全部的属性设置为private。 这能够防止客户端得到对属性引用的可变对象的访问权限并直接修改这些对象。 虽然技术上容许不可变类具备包含基本类型数值的公共final属性或对不可变对象的引用,但不建议这样作,由于它不容许在之后的版本中更改内部表示(项目15和16)。
  5. 确保对任何可变组件的互斥访问。 若是你的类有任何引用可变对象的属性,请确保该类的客户端没法得到对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和readObject方法(条目 88)中进行防护性拷贝(条目 50)。

之前条目中的许多示例类都是不可变的。 其中这样的类是条目 11中的PhoneNumber类,它具备每一个属性的访问方法(accessors),但没有相应的设值方法(mutators)。 这是一个稍微复杂一点的例子:缓存

// 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)";

    }
}

这个类表明了一个复数(包含实部和虚部的数字)。 除了标准的Object方法以外,它还为实部和虚部提供访问方法,并提供四个基本的算术运算:加法,减法,乘法和除法。 注意算术运算如何建立并返回一个新的Complex实例,而不是修改这个实例。 这种模式被称为函数式方法,由于方法返回将操做数应用于函数的结果,而不修改它们。 与其对应的过程(procedural)或命令(imperative)的方法相对比,在这种方法中,将一个过程做用在操做数上,致使其状态改变。 请注意,方法名称是介词(如plus)而不是动词(如add)。 这强调了方法不会改变对象的值的事实。 BigIntegerBigDecimal类没有遵照这个命名约定,并致使许多使用错误。安全

若是你不熟悉函数式方法,可能会显得不天然,但它具备不变性,具备许多优势。 不可变对象很简单。 一个不可变的对象能够彻底处于一种状态,也就是被建立时的状态。 若是确保全部的构造方法都创建了类不变量,那么就保证这些不变量在任什么时候候都保持不变,使用此类的程序员无需再作额外的工做。 另外一方面,可变对象能够具备任意复杂的状态空间。 若是文档没有提供由设置(mutator)方法执行的状态转换的精确描述,那么可靠地使用可变类多是困难的或不可能的。ide

不可变对象本质上是线程安全的; 它们不须要同步。 被多个线程同时访问它们时并不会被破坏。 这是实现线程安全的最简单方法。 因为没有线程能够观察到另外一个线程对不可变对象的影响,因此不可变对象能够被自由地共享。 所以,不可变类应鼓励客户端尽量重用现有的实例。 一个简单的方法是为经常使用的值提供公共的静态 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);

这种方法能够更进一步。 一个不可变的类能够提供静态的工厂(条目 1)来缓存常常被请求的实例,以免在现有的实例中建立新的实例。 全部基本类型的包装类和BigInteger类都是这样作的。 使用这样的静态工厂会使客户端共享实例而不是建立新实例,从而减小内存占用和垃圾回收成本。 在设计新类时,选择静态工厂代替公共构造方法,能够在之后增长缓存的灵活性,而不须要修改客户端。性能

不可变对象能够自由分享的结果是,你永远不须要作出防护性拷贝( defensive copies)(条目 50)。 事实上,永远不须要作任何拷贝,由于这些拷贝永远等于原始对象。 所以,你不须要也不该该在一个不可变的类上提供一个clone方法或拷贝构造方法(copy constructor)(条目 13)。 这一点在Java平台的早期阶段还不是很好理解,因此String类有一个拷贝构造方法,可是它应该尽可能不多使用(条目 6)。学习

不只能够共享不可变的对象,并且能够共享内部信息。 例如,BigInteger类在内部使用符号数值表示法。 符号用int值表示,数值用int数组表示。 negate方法生成了一个数值相同但符号相反的新BigInteger实例。 即便它是可变的,也不须要复制数组;新建立的BigInteger指向与原始相同的内部数组。ui

不可变对象为其余对象提供了很好的构件(building blocks),不管是可变的仍是不可变的。 若是知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象能够构成Map对象的键和Set的元素,一旦不可变对象做为Map的键或Set里的元素,即便破坏了MapSet的不可变性,但不用担忧它们的值会发生变化。

不可变对象提供了免费的原子失败机制(条目 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)”,它用来加速多步操做,好比模幂运算( modular exponentiation)。出于前面所述的全部缘由,使用可变伙伴类比使用BigInteger要困可贵多。 幸运的是,你没必要使用它:BigInteger类的实现者为你作了不少努力。

若是你能够准确预测客户端要在你的不可变类上执行哪些复杂的操做,那么包级私有可变伙伴类的方式能够正常工做。若是不是的话,那么最好的办法就是提供一个公开的可变伙伴类。 这种方法在Java平台类库中的主要例子是String类,它的可变伙伴类是StringBuilder(及其过期的前身StringBuffer类)。

如今你已经知道如何建立一个不可改变类,而且了解不变性的优势和缺点,下面咱们来讨论几个设计方案。 回想一下,为了保证不变性,一个类不得容许子类化。 这能够经过使类用 final 修饰,可是还有另一个更灵活的选择。 而不是使不可变类设置为 final,可使其全部的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法(条目 1)。 为了具体说明这种方法,下面以Complex为例,看看如何使用这种方法:

// 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](http://this.re) = re;

        [this.im](http://this.im) = im;

    }

    public static Complex valueOf(double re, double im) {

        return new Complex(re, im);

    }

    ... // Remainder unchanged

}

这种方法每每是最好的选择。 这是最灵活的,由于它容许使用多个包级私有实现类。 对于驻留在包以外的客户端,不可变类其实是final的,由于不可能继承来自另外一个包的类,而且缺乏公共或受保护的构造方法。 除了容许多个实现类的灵活性之外,这种方法还能够经过改进静态工厂的对象缓存功能来调整后续版本中类的性能。

BigIntegerBigDecimal被写入时,不可变类必须是有效的final,所以它们的全部方法均可能被重写。不幸的是,在保持向后兼容性的同时,这一事实没法纠正。若是你编写一个安全性取决于来自不受信任的客户端的BigInteger或BigDecimal参数的不变类时,则必须检查该参数是“真实的”BigInteger仍是BigDecimal,而不该该是不受信任的子类的实例。若是是后者,则必须在假设多是可变的状况下保护性拷贝(defensively copy)(条目 50):

public static BigInteger safeInstance(BigInteger val) {

    return val.getClass() == BigInteger.class ?
            val : new BigInteger(val.toByteArray());
}

在本条目开头关于不可变类的规则说明,没有方法能够修改对象,而且它的全部属性必须是final的。事实上,这些规则比实际须要的要强硬一些,其实能够有所放松来提升性能。 事实上,任何方法都不能在对象的状态中产生外部可见的变化。 然而,一些不可变类具备一个或多个非final属性,在第一次须要时将开销昂贵的计算结果缓存在这些属性中。 若是再次请求相同的值,则返回缓存的值,从而节省了从新计算的成本。 这个技巧的做用偏偏是由于对象是不可变的,这保证了若是重复的话,计算会获得相同的结果。

例如,PhoneNumber类的hashCode方法(第53页的条目 11)在第一次调用改方法时计算哈希码,并在再次调用时对其进行缓存。 这种延迟初始化(条目 83)的一个例子,String类也使用到了。

关于序列化应该加上一个警告。 若是你选择使您的不可变类实现Serializable接口,而且它包含一个或多个引用可变对象的属性,则必须提供显式的readObjectreadResolve方法,或者使用ObjectOutputStream.writeUnsharedObjectInputStream.readUnshared方法,即默认的序列化形式也是能够接受的。 不然攻击者可能会建立一个可变的类的实例。 这个主题会在条目 88中会详细介绍。

总而言之,坚定不要为每一个属性编写一个get方法后再编写一个对应的set方法。 除非有充分的理由使类成为可变类,不然类应该是不可变的。 不可变类提供了许多优势,惟一的缺点是在某些状况下可能会出现性能问题。 你应该始终使用较小的值对象(如PhoneNumberComplex),使其不可变。 (Java平台类库中有几个类,如java.util.Datejava.awt.Point,本应该是不可变的,但实际上并非)。你应该认真考虑建立更大的值对象,例如StringBigInteger ,设成不可改变的。 只有当你确认有必要实现使人满意的性能(条目 67)时,才应该为不可改变类提供一个公开的可变伙伴类。

对于一些类来讲,不变性是不切实际的。若是一个类不能设计为不可变类,那么也要尽量地限制它的可变性。减小对象能够存在的状态数量,能够更容易地分析对象,以及下降出错的可能性。所以,除非有足够的理由把属性设置为非 final 的状况下,不然应该每一个属性都设置为 final 的。把本条目的建议与条目15的建议结合起来,你天然的倾向就是:除非有充分的理由不这样作,不然应该把每一个属性声明为私有final的

构造方法应该建立彻底初始化的对象,并创建全部的不变性。 除非有使人信服的理由,不然不要提供独立于构造方法或静态工厂的公共初始化方法。 一样,不要提供一个“reinitialize”方法,使对象能够被重用,就好像它是用不一样的初始状态构建的。 这样的方法一般以增长的复杂度为代价,仅仅提供不多的性能优点。

CountDownLatch类是这些原理的例证。 它是可变的,但它的状态空间有意保持最小范围内。 建立一个实例,使用它一次,并完成:一旦countdown锁的计数器已经达到零,不能再重用它。

在这个条目中,应该添加关于Complex类的最后一个注释。 这个例子只是为了说明不变性。 这不是一个工业强度复杂的复数实现。 它对复数使用了乘法和除法的标准公式,这些公式不正确会进行不正确的四舍五入,没有为复数的NaN和无穷大提供良好的语义[Kahan91,Smith62,Thomas94]。

相关文章
相关标签/搜索