欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-01-01 10:46:40
...

保护性拷贝

大家都知道,相比于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()方法来复制不可信的方法参数

上一篇:

下一篇: