保持最小的可变性
说一个类"不变",就是说它的所有对象在运行时不可以被更改。Java系统库中有很多这样的例子,比如String、类封装的基本数据类型、BigInteger和BigDecimal等等。想创建一个不变类,需要遵循下面5条原则:
1. 不能提供任何改变对象状态的方法(比如setter)。
2. 保证类不能被继承,否则子类将可以通过覆盖父类方法来改变对象状态。常用的做法是把类声明为final,还有另一种方法是隐藏或保护所有的构造函数,只提供一个静态factory方法来创建对象(参见item 1)。
3. 所有属性都声明为final。
4. 所有属性都声明为私有。

5. 独占对所有"可变"组件的访问。如果类中有指向可变对象的引用作为属性,那么要保证这些引用不会被用户获得。永远不要把这些引用赋值给用户提供的变量,也不能通过getter返回这样的引用。还应注意在构造函数、访问方法和readObject方法中做"保护性复制"。
前面几节中有很多例子是不变类,比如item 9中的电话号码类。下面是一个复杂些的例子:

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

这个例子中,所有属性都没有setter,只有getter。而且四种运算都被定义成返回一个新对象,而非修改本对象的值,这种编程思想被称为函数式编程(functional programming),与传统的过程式编程(procedural programming)相对。

上述第2条规则有两个反例:BigInteger和BigDecimal。由于这两个类被实现的时候,Java的早期开发者们还没意识到这条规则的重要性,所以它们被设计成可以被继承的,之后出于向下兼容的需要,也不能再做出修改了。所以在程序的安全性依赖于这两个类时,必须验证得到的对象确实是这两个类的对象,而非不受信任的子类:

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

此外,第3条规则过强了。事实上,对于一个不变对象,只有外部可见的数据需要被保护。比如对象内部一些开销较大的运算,其结果可以被缓存起来以优化性能,这些数据可以不是final。
在可能发生序列化(serialization)的环境,要注意readObject或readResolve方法的覆盖,以保证对象不变。
不变对象有如下特点:
1. 简单。对象一旦创建,在整个运行中都不会发生改变,因而使用时不必管理其状态。
2. 天然线程安全,不需要作同步处理,所以可以被自由共用。基于这个特点,不变对象很容易通过对象池管理重用,节约运行资源。比如上面的复数类,完全可以包含以下这样的常量:

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

这样的代码易读易用。
3. 除了不变对象本身,它们的内部数据同样可以被共用。比如BigInteger类,实际上是由一个符号(int),和大小(一个int数组)来表示。那么在对某个BigInteger求负值操作时,就只需将其符号取反,并使大小指向原对象对应的数组即可,不用将所有数据复制。
4. 利用不变对象,可以很好地构造其它对象。比如作为各种map的键(key),或者作为各种set的值。
5. 不变类有唯一一个缺点:对应每个不同的值,都要有一个单独的对象,有时建立这些对象开销巨大。比如,对于一个百万比特的BigInteger,要改变其最低一个比特:

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

flipBit方法会建立一个全新的百万比特BigInteger对象,而这个新对象与原对象相比,只有最低一个比特不同。为解决这种性能问题,可为不变类提供一个可变对应类。比如StringBuffer和StringBuilder之于String,或者BitSet之于BigInteger。
鉴于不变类的优点,开发者应尽可能使用它们。只在有明确需求时,才创建可变类。同样,在任何一个类之内,尽可能使属性成为final,只在有明确需求时,才将属性声明为可变。

提交评论


安全码
刷新