【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;
}
}
有人可能会考虑将start
和end
的拷贝使用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
方法。
另外,就是由于安全原因。在上面的代码中,对于start
和end
的clone
操作,如果客户端传递的代码是继承了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()方法来复制不可信的方法参数。