×

消息

EU e-Privacy Directive

This website uses cookies to manage authentication, navigation, and other functions. By using our website, you agree that we can place these types of cookies on your device.

View e-Privacy Directive Documents

You have declined cookies. This decision can be reversed.

保持最小的可变性
说一个类"不变",就是说它的所有对象在运行时不可以被更改。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,只在有明确需求时,才将属性声明为可变。

提交评论


安全码
刷新