非可变类,这个名词可能并不是所有人都知道。所谓非可变类,就是具有如下特征的类:每个实例中包含的所有信息都必须在该实例被创建的时候就提供出来;并且在此对象的整个生命周期内固定不变。
Java平台中其实有很多非可变类,比如原语类型的包装类(Integer、Long、Short等)、String、BigDecimal、BigInteger等。
使用非可变类的理由是:比起可变类,它们更加易于设计、实现和使用,更不容易出错,更加安全。
为了使一个类成为非可变类,要遵循以下五个规则:
1)不要提供任何会修改对象的方法。
2)保证没有可被子类改写的方法。(一般的做法是使本类成为final的)
3)使所有的域都是final的。(最好连类都是final的等同效果(final或者所有构造函数私有化),避免使用者派生出不合规范的子类)
4)使所有的域都是private的。
5)保证对于任何可变组件的互斥访问。
解释一下第5条,首先如果在你的类中有可变对象的域,则必须确保使用者无法获得这个对象的引用。具体说就是不能提供公有的getter方法,否则,如:
public class MyImmutable {
private final java.lang.Date birthday;
public Date getBirthday() {
return this.birthday;
}
}
//那么使用者可以这样来更改birthday的内容:(应为java.lang.Date是可变类,这一点是个遗憾,后面会提到)
MyImmutable mi = new MyImmutable(...);
mi.getBirthday().setYear(2000);
// 这就破坏了mi的非可变性。
再有就是在构造函数、访问方法和readObject方法(见【第56条】)中使用保护性拷贝技术。(见【第24条】)
如果类似上面的getter方法是必要的,那么要这样写:
public Date getBirthday() {
return new Date(this.birthday); // 用birthday做实参构造一个新的、和birthday同内容而不同地址的新实例,并返回
}
在非可变类的任何一个方法中,切忌不能改变内部信息。在来看看【第8条】中提及到的复数Complex的例子,以复数的加法为例,可千万不能再这样写了:
public void add(Complex c) {
this.re += c.re;
this.im += c.im;
}
对于非可变类,add方法必须要返回一个新的实例,而不再是在原来的基础上修改:
public Complex add(Complex c) {
return new Complex(this.re + c.re, this.im + c.im);
}
非可变类的好处除了简单(要有5大条条框框,真的简单吗?)以外,还有就是它是线程安全的,他们不要求同步(当然了,因为“不可能改变”吗),因此可以被自由地共享。
另外,还有一个好处就是可以节省系统内存开销。举个例子,String是非可变类,我们先假设它是可变类。如果将数据库中储存的全中国所有人的姓名都已String的形式一一保存,那么将要在内存中开销掉13亿个String的实例。幸好String是非可变的,所以内存开销可能仅有13亿的十分之一。因为中国人有大量的重名,那些重名的仅仅会被实例化一次。你可以做这样一个试验:
String s1 = "abc";
String s2 = "xyz";
String s3 = "abc";
通过Eclipse的调试工具,你可以看到,s1和s3的地址引用是相同的,也就是说系统并没有为s1和s3各分配一块空间,而是使用的同一个实例化的空间。
如果把这个例子中的“姓名”换成“生日”,并用java.lang.Date型来保存,那么不幸的事情发生了:假设现在在世的所有中国人中最大年龄为100岁,且近100年中,每天都会有目前健在的人出生,那么所有中国人不重复的生日可能性就是365 * 100 + 25(闰年) = 36525。然而,事实上你却会看到内存中真的出现了13亿个实例化的Data对象,而不是36525个。这就是因为前面提到的,java.lang.Date并不是非可变类。这是一个历史遗留下来的遗憾——在java系统设计之初,那时候的牛人们也还没有意识到非可变类的好处和必要性,所以把Date型设计成可变类了。后来,即使悔悟了,但是由于要保持向前兼容性而不得不继续这样。从一点也能看出上一条(【第11条】)的重要性。(当然Date设计出来就是让别人用的)
最后说一说非可变类的缺点,可能只有一条——对于每一个不同的值都要求一个单独的对象。创建这样的类可能会代价很高,特别是对于大型对象的情况。综合这一点和前面的优点,我总结一下,就是在那些容易出现重复值,而且并经常会改变值的应用中,适合使用非可变类。
如果一个类的实例在被创建后,还要频繁的改变其值,就不合适用非可变类,例如:
public String toString(){
String strValue = "";
strValue += this.f1.toString();
strValue += this.f2.toString();
strValue += this.f3.toString();
strValue += this.f4.toString();
strValue += this.f5.toString();
strValue += this.f6.toString();
strValue += this.f7.toString();
return strValue;
}
不停地改变String对象的值,其实是在不停地创建新的实例,并由JVM(Java虚拟机)去回收旧的实例,这样的性能很差。
toString方法应该这样写:
public String toString(){
StringBuffer sb = new StringBuffer(1000); // 简单的例子,就不考虑缓冲区溢出的问题了
sb.append(this.f1.toString());
sb.append(this.f2.toString());
sb.append(this.f3.toString());
sb.append(this.f4.toString());
sb.append(this.f5.toString());
sb.append(this.f6.toString());
sb.append(this.f7.toString());
return sb.toString();
}
StringBuffer是可变类,append方法就是直接改变它的值,而不必创建新的实例。
还有一句总结的话,就是“所有的值类都应该考虑使用非可变类”。但也不是绝对的,JavaBean应该也广义地算是值类。可是我们通常用一个JavaBean来保存一个客户订单,然而订单是经常要被修改的(如不停地在改变状态),显然使用非可变类每次改变都重新创建一个新的实例是不明智的(这就属于大型对象)。
同样,还有一句总结性的话,比上一句更“恐怖”:“除非有很好的理由要让一个类成为可变类,否则就应该是非可变的” 。而其,“即便一个类不能被做成非可变的,你仍然应该尽可能地限制它的可变性”。
【Effective Java 学习笔记】系列连载专题请见:
http://tonylian.iteye.com/categories/64208