JAVA中如何写一个不可变类
Effective Java 第39条中提到:必要时进行保护性拷贝
书中给到的一个例子,总结一句就是:成员变量不要被外部引用直接赋值,而是拷贝之后的赋值。具体看下面例子。
1 public class Period { 2 private final Date start; 3 private final Date end; 4 5 public Period(Date start, Date end){ 6 if(start.compareTo(end) > 0){ 7 throw new IllegalArgumentException(start + " after " + end); 8 } 9 this.start = start; 10 this.end = end; 11 } 12 13 public Date getStart() { 14 return start; 15 } 16 17 public Date getEnd() { 18 return end; 19 } 20 21 public static void main(String[] args) { 22 Date start = new Date(); 23 Date end = new Date(); 24 Period p = new Period(start, end); 25 end.setYear(78);//这里改变了外部引用指向的值 26 System.out.println(p.getStart()); 27 System.out.println(p.getEnd()); 28 } 29 30 31 32 }
从代码中我们可以看到,成员变量是被final修饰的,值是不可变的,且只有一个构造函数初始化它们的值,而构造函数里面又有对start,end这两个进行判断,即不让end<start。
但是,构造函数中,对成员变量被外部引用直接赋值。因为传入的是引用,所以这个被final修饰的成员变量的值,不变的是引用,但是这个外部引用所指向的值还是可变的。
从代码25行可以看出,外部引用指向的值被修改了,而成员变量end与外部引用end都是指向同一个内存的,所以成员变量end指向的值也变了。我们就可以很简单的把end<start。
这样代码就出现了bug.
解决这个BUG的方法就是:把外部引用拷贝之后赋值
代码修改如下
1 public Period(Date start, Date end){ 2 this.start = new Date(start.getTime());//把外部引用拷贝后再赋值 3 this.end = new Date(start.getTime()); 4 if(start.compareTo(end) > 0){ 5 throw new IllegalArgumentException(start + " after " + end); 6 } 7 /*******************修改前代码如下*****************/ 8 // if(start.compareTo(end) > 0){ 9 // throw new IllegalArgumentException(start + " after " + end); 10 // } 11 // this.start = start;//不要被外部引用直接赋值 12 // this.end = end; 13 }
改到这里,还有一个问题。我们还把成员变量的引用通过getter直接暴露出来了。这还是会出现问题
因为我们还可以通过getter获取成员变量的引用,再去改变它内部的值
代码如下
1 Date start = new Date(); 2 Date end = new Date(); 3 Period p = new Period(start, end); 4 System.out.println(p.getStart()); 5 System.out.println(p.getEnd()); 6 //通过上面的改造,我们已经不能通过改变end.setYear();去影响成员变量的值了,但是下面这句还是可以改变成员变量的值, 7 p.getStart().setYear(88);//这里getter返回的是一个引用,我们可以通过引用去改变成员变量的值
解决这个BUG的方法 就是:getter返回可变内部域的保护性拷贝
把getter做如下修改即可
1 public Date getStart() { 2 return new Date(this.start.getTime());//正确做法:返回可变内部域的保护性拷贝 3 //return this.start;//错误做法:直接返回引用 4 } 5 6 public Date getEnd() { 7 return new Date(this.end.getTime());//正确做法:返回可变内部域的保护性拷贝 8 //return this.end;//错误做法:直接返回引用 9 }
总结:
1)对于赋值,我们不要把外部引用直接赋值给成员变量
2)对于getter,我们不要把成员变量的引用直接返回