表示泄露:
对于一个ADT来说,表示泄露是一项要坚决避免的事情。表示泄露就是指用户不通过我们ADT提供的方法,就可以修改ADT的内容,出现不必要的麻烦。
例如public的属性是一种表示泄露,mutable类型的数据直接传输是一种表示泄露。
我们会采用包括但不限于private final、防御式拷贝等方式来避免表示泄露。
今天我想讨论一下和List有关的表示泄露问题。
List< Immutable>
如果List当中存放的是Immutable类型的数据,我们采用防御式拷贝的方式就可以处理好表示泄露的问题。因为只要采用了防御式拷贝,对新数组的变化就不会影响到原数组。而Immutable的数据完全打消了我们对于多引用的顾虑。
防御式拷贝需要消耗额外的存储空间,还会拖慢程序的运行速度。我们还可以用Collections.unmodifiableList来进行表示泄露的防御。
下面介绍一下Collections.unmodifiableList
Collections.unmodifiableList
Collections.unmodifiableList是一个decorator,它是对我们常规List的一个包装。使用方法如下:
List<Mutable> list = new ArrayList<>();
。。。
List<Mutable> safe = Collections.unmodifiableList(new ArrayList<>(list));
生成的safe不支持“修改”
下面我们从源码追踪不可修改的原因。我们截取源码的一部分
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
public boolean addAll(int index, Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
public ListIterator<E> listIterator(final int index) {
return new ListIterator<E>() {
private final ListIterator<? extends E> i
= list.listIterator(index);
public boolean hasNext() {return i.hasNext();}
public E next() {return i.next();}
public boolean hasPrevious() {return i.hasPrevious();}
public E previous() {return i.previous();}
public int nextIndex() {return i.nextIndex();}
public int previousIndex() {return i.previousIndex();}
public void remove() {//注意看这里
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
i.forEachRemaining(action);
}
};
}
可以看出,所有List的增删改方法都直接会抛出异常,就连Iterator的remove方法也没能逃过一劫。所以Collections.unmodifiableList可以实现一定程度的保护。
List< Mutable>
面对Mutable对象,防御式拷贝和Collections.unmodifiableList全都无能为力。我们用一个小实验来看。
//一个简单的mutable对象
public class Mutable {
private int num;
public Mutable(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
public class TestList {
public static void main(String[] args) {
List<Mutable> list = new ArrayList<>();
Mutable tmp = new Mutable(1);
list.add(tmp);
System.out.println("初始值:" + list.get(0).getNum());
List<Mutable> copy = new ArrayList<>(list);
List<Mutable> safe = Collections.unmodifiableList(new ArrayList<>(list));
copy.get(0).setNum(2);
System.out.println("利用防御式拷贝修改后原list值:" + list.get(0).getNum());
safe.get(0).setNum(3);
System.out.println("利用unmodifiablelist修改后原list值:" + list.get(0).getNum());
}
}
得到输出:
初始值:1
利用防御式拷贝修改后原list值:2
利用unmodifiablelist修改后原list值:3
可见所有的方式都可以对原数组修改,表示泄露了。
这里我们所作的修改,不是基于List所提供的方法,而是直接对Mutable类型调用其mutator方法进行修改。我们的List存储的都是一个一个的引用值,而mutator方法和引用无关,它直接改变引用所在内存的内容。所以出现这种表示泄露。
因此除了List之外,我们可以延伸到所有Mutable的类型。只要Mutable的引用暴漏在ADT外部,表示泄露就无法避免。
解决方案:
- 对Mutable的类型构造深拷贝函数,再利用防御式拷贝的List。这样每一个对象创建新的地址就不怕修改。
- 禁止对Mutable类型的直接暴漏。例如一个管理Piece棋子对象的Board棋盘,Piece是mutable的,那我们只允许客户端在外部用棋子名称String进行操作而不允许直接暴露Piece对象。