第三章 Immutable Type
在到处都是引用的Java世界里,要构造一个不可变类型可没有想像中的那么简单。
1.Fundamentals
public class ImmutablePerson
{
private String name;
private int age;
public ImmutablePerson(String name,int age){
this.name = name;
this.age = age;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
}
这个类看似是个不可变类型,其实不然。我们完全可以用Reflection来改变ImmutablePerson类型的对象。要使其成为真正的不可变类型,
应该这样改:
public class ImmutablePerson
{
private final String name;
private final int age;
public ImmutablePerson(String name,int age){
this.name = name;
this.age = age;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
}
不要以为final就可以解决所有的问题,这只是针对原始类型(String虽不是原始类型,但其表现更像是原始类型),要不看看下面的这个类:
public class Circle
{
private final Point point;
public Circle(Point point){
this.point = point;
}
public Point getPoint(){
return this.point;
}
}
这个程序至少有两个漏洞(对不变类型来说)。首先是它的构造函数,看下面的调用:
public void test(){
Point p = new Point(20,30);
Circle c = new Circle(p);
System.out.println(c.getPoint().toString());
p.x = 25;
p.y = 35;
System.out.println(c.getPoint().toString());
}
这是运行结果:
java.awt.Point[x=20,y=30]
java.awt.Point[x=25,y=35]
原因就在于你给外人留下了‘把柄’。同样,方法getPoint()也存在同样的问题,这次你把‘把柄’主动送给了别人。要注意final只能防止给变量赋新值,不能阻止你调用变量的方法来改变变量。
要想不给别人留下把柄,最好是每次赋值时都拷贝这个变量,如下:
public class Circle
{
private final Point point;
public Circle(Point point){
this.point = new Point(point);
}
public Point getPoint(){
return new Point(this.point);
}
}
其实很多时候即使是非不可变类型也要这么做,给他人留下‘把柄’可不是什么好习惯。
2.The String Trap
在java中String是一个不可变类型,每次改变String的操作都会导致新的String对象生成,如果在程序中大量对String操作,会消耗掉大量的内存,如:
public String toString(String[] array){
String field = "[";
for(int i=0;i<array.length;++i){
field += array[i] + ",";
}
field += "]";
return field
}
循环里的每次‘+’操作都会产生一个新的String对象,所以对String的操作并不象想像中的那么‘好用’。
要对大量的字符串做连接最好是使用StringBuffer,如上例可以这样改:
public String toString(String[] array){
StringBuffer field = new StringBuffer();
field.append("[");
for(int i=0;i<array.length;++i){
field.append(array[i] + ",");
}
field.append("]");
return field.toString();
}
但事实上这里仍然有问题,因为StringBuffer的文档中说在使用StringBuffer时,如果使用‘+’来连接String,那么java虚拟机会自动创建一个StringBuffer来做这个连接(在我的jdk1.5中我并没有看到类似这个的提示,哪位如果找到了告诉我),也就是说上例中每次循环你都重新创建了一个StringBuffer类。遗憾的是在jdk1.5中这个说法似乎并不成立,而且我也没有找到作者所说的提示,可能只是在jdk1.5以前的版本中是这样的吧(手中没有jdk1.4的文档了),这是我对这个说法的测试:
public class Test
{
public void asString1(String[] array){
String field = "[";
for(int i=0;i<array.length;++i){
field += "["+i+"]"+array[i] + ",";
}
field += "]";
}
public void asString2(String[] array){
StringBuffer field = new StringBuffer();
field.append("[");
for(int i=0;i<array.length;++i){
field.append("["+i+"]"+array[i] + ",");
}
field.append("]");
}
public void asString3(String[] array){
StringBuffer field = new StringBuffer();
field.append("[");
for(int i=0;i<array.length;++i){
field.append("[");
field.append(i);
field.append("]");
field.append(array[i]);
field.append(",");
}
field.append("]");
}
public static void main(String[] args){
Test t= new Test();
String[] array = new String[5000];
for(int i=0;i<array.length;++i){
array[i] = new String("test " + i);
}
long time = System.currentTimeMillis();
t.asString1(array);
System.out.println(System.currentTimeMillis()-time);
time = System.currentTimeMillis();
t.asString2(array);
System.out.println(System.currentTimeMillis()-time);
time = System.currentTimeMillis();
t.asString3(array);
System.out.println(System.currentTimeMillis()-time);
}
}
在我机器(AMD1800+)上的运行的结果如下:
E:/program>java Test
40418
20
40
可见第三种方法要比第二种方法费时,这与书中所说并不相符。