【Effective Java】条39:必要时使用保护性拷贝

保护性拷贝

大家都知道,相比于C或者C++Java是一门安全性的语言。但这不意味着在编程时你可以随意为之,相反,你也不得不尽最大考虑客户端代码在尽力破坏你的不可变变量等,你也必须保护性的来设计自己的程序。如:

public final class PeriodV1 {

  private final Date start;
  private final Date end;

  public PeriodV1(Date start, Date end) {
    if (start.compareTo(end) > 0) {
      throw new IllegalArgumentException();
    }

    this.start = start;
    this.end = end;
  }

  public Date getStart() {
    return start;
  }

  public Date getEnd() {
    return end;
  }
}

上面程序表示构建一个不可变的时间段,在构建时要确保start时间不会晚于end的时间。程序看起来没什么问题,但是真的安全吗?当然不安全。看下面的测试:

public class PeriodDemo {

  public static void main(String[] args) {
    PeriodDemo demo = new PeriodDemo();
    demo.periodV1();
  }

  //输出结果为:
  //=====PeriodV1=====
  //Original start & end: 
  //start = Fri May 25 11:18:54 CST 2018
  //end = Fri May 25 11:18:54 CST 2018
  //After modify start & end: 
  //start = Fri May 25 10:46:34 CST 2018
  //end = Fri May 25 11:18:54 CST 2018
  private void periodV1() {
    Date start = new Date();
    Date end = new Date();

    System.out.println("=====PeriodV1=====");

    System.out.println("Original start & end: ");
    System.out.println("start = " + start);
    System.out.println("end = " + end);

    PeriodV1 periodV1 = new PeriodV1(start, end);
    //修改原始数据的值
    start.setTime(1527216394000L);

    System.out.println("After modify start & end: ");
    System.out.println("start = " + periodV1.getStart());
    System.out.println("end = " + periodV1.getEnd());
  }
}

发现通过修改原始的数据会修改PeriodV1里面的原本不可变变量,说明是不够安全的。

好的,既然通过修改原始变量就可以修改,那我们对构造函数进行修改。代码如下:

public final class PeriodV2 {

  private final Date start;
  private final Date end;

  //先拷贝数据,再用类成员校验
  public PeriodV2(Date start, Date end) {
    this.start = start;
    this.end = end;

    if (this.start.compareTo(this.end) > 0) {
      throw new IllegalArgumentException();
    }
  }

  public Date getStart() {
    return start;
  }

  public Date getEnd() {
    return end;
  }
}

经过这次修改,发现通过修改原始start的值已经不能够改变PeriodV2成员start的值了,初步有了保护性。但是有点不足,如下测试:

public class PeriodDemo {

  public static void main(String[] args) {
    PeriodDemo demo = new PeriodDemo();
    demo.periodV2();
  }

  //输出结果为:
  //=====PeriodV1=====
  //Original start & end: 
  //start = Fri May 25 11:18:54 CST 2018
  //end = Fri May 25 11:18:54 CST 2018
  //After modify start & end: 
  //start = Fri May 25 10:46:34 CST 2018
  //end = Fri May 25 11:18:54 CST 2018
  private void periodV2() {
    Date start = new Date();
    Date end = new Date();

    System.out.println("=====PeriodV2=====");

    System.out.println("Original start & end: ");
    System.out.println("start = " + start);
    System.out.println("end = " + end);

    PeriodV2 periodV2 = new PeriodV2(start, end);
    //通过获取类成员再进行修改
    periodV2.getStart().setTime(1527216394000L);

    System.out.println("After modify start & end: ");
    System.out.println("start = " + periodV2.getStart());
    System.out.println("end = " + periodV2.getEnd());
  }
}

这次是通过PeriodV2提供的getter方法对参数进行修改的。没办法,还是不安全,只能将getter方法里返回的变量进行再包装了。如下:

public final class PeriodV3 {

  private final Date start;
  private final Date end;

  public PeriodV3(Date start, Date end) {
    this.start = start;
    this.end = end;

    if (this.start.compareTo(this.end) > 0) {
      throw new IllegalArgumentException();
    }
  }

  public Date getStart() {
    return new Date(start.getTime());
  }

  public Date getEnd() {
    return new Date(end.getTime());
  }
}

这时候,才可以说相比于之前的两个版本,要安全了。

保护性拷贝应用场景

保护性拷贝常常应用在不可变类中,防止类的成员被修改。其实任何时候,当你的方法或者构造函数接收的参数是由客户端传递的,你应该想想,我的方法是否可以接受此参数在外部被修改,不可以,那就应该采用保护性拷贝。另外,当返回类的成员时,也应该考虑该类是否为不可变类,如果是的话则返回的成员需要是新构建的,而不能直接返回类内部成员的引用。

但是需要注意的是,保护性拷贝是有性能上的损失的。如果一个类能确定客户端的调用不会修改类内部成员,那么可以不用保护性拷贝,但是需要在函数注释中注明这一切。

再说clone

在上面修改构造函数的代码中:

public final class PeriodV2 {

  private final Date start;
  private final Date end;

  //先拷贝数据,再用类成员校验
  public PeriodV2(Date start, Date end) {
    this.start = start;
    this.end = end;

    if (this.start.compareTo(this.end) > 0) {
      throw new IllegalArgumentException();
    }
  }

  public Date getStart() {
    return start;
  }

  public Date getEnd() {
    return end;
  }
}

有人可能会考虑将startend的拷贝使用clone进行:

//先拷贝数据,再用类成员校验
public PeriodV2(Date start, Date end) {
  this.start = start.clone();
  this.end = end.clone();

  if (this.start.compareTo(this.end) > 0) {
    throw new IllegalArgumentException();
  }
}

再次声明,强烈禁止。早在【Effective Java】条11:谨慎覆盖clone方法中也说了,需谨慎使用`clone方法。

另外,就是由于安全原因。在上面的代码中,对于startendclone操作,如果客户端传递的代码是继承了Date的子类,且重写了clone方法,如:

public class SubDate extends Date {

  private static List<Object> instances = Lists.newArrayList();

  public static List<Object> getInstances() {
    return instances;
  }

  @Override
  public Object clone() {
    System.out.println("SubDate clone");
    return super.clone();
  }
}

那么在调用PeriodV4的构造函数时,其实调用的是SubDate中的clone方法,至于子类中的clone会做什么,那就不受控制了。进一步关于clone的漏洞可以参考:《Java编码指南:编写安全可靠程序的75条建议》—— 指南10:不要使用clone()方法来复制不可信的方法参数

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值