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

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

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

不可变对象为其他对象提供了很好的构件(building blocks),无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成Map对象的键和Set的元素,一旦不可变对象作为Map的键或Set里的元素,即使破坏了Map和Set的不可变性,但不用担心它们的值会发生变化。

不可变对象提供了免费的原子失败机制(条目 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类的实现者为你做了很多努力。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyypjx.html