最近在阅读《Effective JAVA》,感谢它让我重温了很多知识点。不过有些章节不是一时半会就能全懂,也是要多回头再看,结合一些实际经验就好理解了。今天看到“必要时进行保护性拷贝”有感,记录一下。
JAVA是一门面对向象的语言,对象作为主体。对象中可能有不少内部组件,比如List,map等。按照惯例,我们可能会为一些属性提供setter和getter。这个时候我们可能就越过了对象控制属性,而是直接操作了。另外从安全性上讲,对外提供的查询类型的方法,是不允许修改到组件内部来的。因为对外的方法返回的是内部组件对象的引用,客户端在不可控的时候,有可能破坏。
如下面的代码,声称可以表示一段不可变的时间。粗略看start和end都有比较先后,并且都是final的。
public final class Period {
private final Date start;
private final Date end;
/**
* @param start
* the beginning of the period
* @param end
* the end of the period; must not precede start
* @throws IllegalArgumentException
* if start is after end
* @throws NullPointerException
* if start or end is null
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
// Repaired constructor - makes defensive copies of parameters - Page 185
// Stops first attack
// 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(start +" after "+ end);
// }
public Date start() {
return start;
}
public Date end() {
return end;
}
// Repaired accessors - make defensive copies of internal fields - Page 186
// Stops second attack
// public Date start() {
// return new Date(start.getTime());
// }
//
// public Date end() {
// return new Date(end.getTime());
// }
public String toString() {
return start + " - " + end;
}
// Remainder omitted
}
然而,Date类本身是可变的。
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
// 修改了end对象,也间接地影响了p对象
end.setYear(2016);
为了保护Period实例的内部不受这种篡改,对构造方法的每个可变参数进行保护性拷贝。
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
使用新对象而不是老对象的引用,这样就切断了通过老对象的修改,间接影响p对象。但是这并不能完全解决问题,还可以通过 p.end().setYear(2016);来修改。两个方法得改。
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
这样子后,Period真的是不可变的了。
import java.util.ArrayList;
public class Student {
private String name;
private ArrayList courses;
public Student(String name, ArrayList courses) {
this.name = name;
this.courses = courses;
}
public ArrayList getCourses() {
return courses;
}
public void setCourses(ArrayList courses) {
this.courses = courses;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("001");
list.add("002");
Student s = new Student("Tom", list);
ArrayList anotherList = s.getCourses();
anotherList.add("999"); // 获取了list的引用,就可以随意增加课程。
System.out.println("Tom's course.length = " + s.getCourses().size());
}
}
稍作修改,首先仅对外提供的getCourses()方法,而没有setCourses()方法,而且通过getCourses()方法获得的courses是“只读的”,如果你试图向其添加一个新课程,则抛出java.lang.UnsupportedOperationException。你必须通过Student1.addCourse()来向特定的Student1对象添加一个新课程。就好像,你必须让顾客自己向购物车里放食物,而不能在顾客毫不知情下,偷偷向其购物车里放食物。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Student {
private String name;
private ArrayList courses;
public Student(String name, ArrayList courses) {
this.name = name;
this.courses = courses;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void addCourse(String course) {
courses.add(course);
}
public String removeCourse(String course) {
boolean removed = courses.remove(courses);
if (removed) {
return course;
} else {
return null;
}
}
public List getCourses() {
return Collections.unmodifiableList(courses);
}
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("001");
list.add("002");
Student s = new Student("Tom", list);
List anotherList = s.getCourses();
/**
* throws java.lang.UnsupportedOperationException should replace with
* s.addCourse(String course)
*/
anotherList.add("999");
// never reached
System.out.println("Tom's course.length = " + s.getCourses().size());
}
}
public int size() {return c.size();}
public boolean isEmpty() {return c.isEmpty();}
public boolean contains(Object o) {return c.contains(o);}
public Object[] toArray() {return c.toArray();}
public T[] toArray(T[] a) {return c.toArray(a);}
public String toString() {return c.toString();}
public boolean add(E e) {
throw new UnsupportedOperationException();
}
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
public boolean containsAll(Collection> coll) {
return c.containsAll(coll);
}
public boolean addAll(Collection extends E> coll) {
throw new UnsupportedOperationException();
}
public boolean removeAll(Collection> coll) {
throw new UnsupportedOperationException();
}
public boolean retainAll(Collection> coll) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}