Java语言是一种比较省力的语言,它对于缓冲区溢出、数组越界、非法指针等都自动免疫,有垃圾自动回收机制等等,比C、C++要安全的多。即使是安全的语言,也有自己的独特机制,也是需要把一些东西进行隔离。我们知道,Java 语言中有八大基本类型和对象类型,基本类型是多少就是多少,没有引用,但对象类型是有个地址值的,存在于堆栈之中,就是这个地址值,可能会随时改变,所以,我们需要对于这种情况做一些防护。看例子:
public final class Period {
private final Date start;
private final Date end;
public Period(Date start,Date end) {
if(start.compareTo(end) > 0){
throw new IllegalArgumentException(start + " after " + end);
}
this.start = start;
this.end = end;
}
public Date start(){
return start;
}
public Date end(){
return end;
}
}
这个类,用了上一章的知识点,对参数进行了检查,结束时间必须比开始时间早,但是,类的两个成员变量都是对象,都是有引用值的,每个对象里面的值是可以改变的,所以
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78);
System.out.println(period.end());
前面三行代码没问题,是创建了两个 Date 对象,然后当做参数传给了Period的构造方法,到此,两个Date的值都是默认值,是相等的;但是第四行代码,对end的Date值进行了赋值,此时,值就改变了,因为是对象,有同一个引用地址值,第四行代码的赋值,导致第三行代码 period 中的 end 对象的值也变了,所以问题就出现了,如果不想让外部改变period的属性的值怎么办?此时可以用保护性拷贝的方法,我们可以在构造方法里,包上一层
public Period(Date start,Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(this.start.compareTo(this.end) > 0){
throw new IllegalArgumentException(this.start + " after " + this.end);
}
}
构造方法里,通过this.start = new Date(start.getTime()); 把原先start的值拿出来,重新new一个新的Date对象,然后把新对象的地址值赋给成员变量this.start,这样,period 的成员变量值start与外面单独生成的start就没关系了,除了date里面的值,也就是日期一样外,其他的就没联系了,外面的start的值怎么改变,都不会影响period成员变量start的值了,因为地址值不一样,不是同一个对象。通过new一个新的对象,并把原对象的值赋给新对象,这就是拷贝,我们没使用clone方法来拷贝,是因为Date是非final的,不能保证clone方法一定正确。这样,问题就初步解决了。构造方法里面的隐患解决了,但还有另外一个地方,
public Date start(){
return start;
}
public Date end(){
return end;
}
这两个方法对外暴露了Date的对象值,通过这两个方法,外面也能获取到地址值,也就是这两个对象,也就可以重新赋值,
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
period.end().setYear(98);
System.out.println(period.end());
注意看第四行代码,我们拿到了period对象中的end属性,然后把它的值给改变了,怎么预防这种问题呢?同样加拷贝。
public Date start(){
return new Date(start);
}
public Date end(){
return new Date(end);
}
这样,即使 period.end() 获取的是另外一个对象,即使重新赋值,也与原对象没关系了,不会造成影响。经过这两次加入拷贝,不论外部怎么恶意修改代码,都不会对我们的Period类的值造成影响。这样的弊端也有一点,就是多次的调用 start() end() 方法时,会产生大量的新的对象,为了解决这个问题,我们可以使用缓存技术,再创建两个Date对象,赋值 new Date(start);返回这两个对象即可,但这样也会额外多造成两个对象,要根据实际情况来判断使用情况。
public final class Period {
private final Date start;
private final Date end;
private final Date start2;
private final Date end2;
public Period(Date start,Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(this.start.compareTo(this.end) > 0){
throw new IllegalArgumentException(this.start + " after " + this.end);
}
start2 = new Date(start.getTime());
end2 = new Date(end.getTime());
}
public Date start(){
return start2;
}
public Date end(){
return end2;
}
}
此外,我们常用到场景就是对于list集合,比如我们要对一个list集合做一些校验,获取一些对象但又不想让外部修改它,我们一般也是用这种方法,例如
public List takeList(){
return new ArrayList(list);
}
我们也会转换一下。如果是一个类的成员变量是对象,我们为了安全,可以选择是否是需要保护性拷贝。如果对象都是基本类型,那就没必要了,因为正常操作下,外部是改变不了的,主要还是对象类型。