第三十九条 必要时进行保护性拷贝

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);
    }
我们也会转换一下。如果是一个类的成员变量是对象,我们为了安全,可以选择是否是需要保护性拷贝。如果对象都是基本类型,那就没必要了,因为正常操作下,外部是改变不了的,主要还是对象类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值