62、迭代器模式(中)
1、如何应对遍历时改变集合导致的未决行为
当通过迭代器来遍历集合的时候,增加、删除集合元素会导致不可预期的遍历结果
实际上 "不可预期" 比直接出错更加可怕,有的时候运行正确,有的时候运行错误,一些隐藏很深、很难 debug 的 bug 就是这么产生的
那我们如何才能避免出现这种不可预期的运行结果呢
有两种比较干脆利索的解决方案:一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错
第一种解决方案比较难实现,我们要确定遍历开始和结束的时间点
- 遍历开始的时间节点我们很容易获得,我们可以把创建迭代器的时间点作为遍历开始的时间点
- 但遍历结束的时间点该如何来确定呢
你可能会说,遍历到最后一个元素的时候就算结束呗,但在实际的软件开发中,每次使用迭代器来遍历元素,并不一定非要把所有元素都遍历一遍
你可能还会说,那我们可以在迭代器类中定义一个新的接口 finishIteration(),主动告知容器迭代器使用完了,你可以增删元素了
但这就要求程序员在使用完迭代器之后要主动调用这个函数,也增加了开发成本,还很容易漏掉
第二种解决方法更加合理,Java 语言就是采用的这种解决方案,增删元素之后,让遍历报错,怎么确定在遍历时候集合有没有增删元素呢
- 在 ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1
- 当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 值传递给迭代器的 expectedModCount 成员变量
- 之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,我们都会检查集合上的 modCount 是否等于 expectedModCount
也就是看,在创建完迭代器之后,modCount 是否改变过
如果两个值不相同,那就说明集合存储的元素已经改变了,要么增加了元素,要么删除了元素
之前创建的迭代器已经不能正确运行了,再继续使用就会产生不可预期的结果
所以我们选择 fail-fast 解决方式,抛出运行时异常,结束掉程序,让程序员尽快修复这个因为不正确使用迭代器而产生的 bug
public class ArrayList<E> implements List<E> {
// ...
private java.util.ArrayList<E> list;
public int modCount; // 修改次数
public ArrayList() {
list = new java.util.ArrayList<>();
modCount = 0;
}
public void add(E e) {
modCount++;
list.add(e);
}
public E remove(int index) {
modCount++;
return list.remove(index);
}
public E get(int index) {
return list.get(index);
}
public int size() {
return list.size();
}
public Iterator<E> iterator() {
return new ArrayIterator<>(this);
}
// 省略其他代码 ...
}
public class ArrayIterator<E> implements Iterator<E> {
private int cursor; // 光标
private ArrayList<E> arrayList;
private int expectedModCount; // 预期修改数
public ArrayIterator(ArrayList<E> arrayList) {
this.cursor = 0;
this.arrayList = arrayList;
this.expectedModCount = arrayList.modCount;
}
@Override
public boolean hasNext() {
checkForCoModification();
return cursor < arrayList.size(); // 注意这里, cursor 在指向最后一个元素的时候, hasNext() 仍旧返回 true
}
@Override
public void next() {
checkForCoModification();
cursor++;
}
@Override
public E currentItem() {
checkForCoModification();
if (cursor >= arrayList.size()) {
throw new NoSuchElementException();
}
return arrayList.get(cursor);
}
/**
* arrayList.modCount == this.expectedModCount
* 检查在遍历的过程中是否增删元素了, 如果增删元素了, 就报错
*/
private void checkForCoModification() {
if (arrayList.modCount != expectedModCount) throw new ConcurrentModificationException();
}
}
2、如何在遍历的同时安全地删除集合元素
像 Java 语言,迭代器类中除了前面提到的几个最基本的方法之外,还定义了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素
不过需要说明的是,它并没有提供添加元素的方法,毕竟迭代器的主要作用是遍历,添加元素放到迭代器里本身就不合适
我个人觉得 Java 迭代器中提供的 remove() 方法还是比较鸡肋的,作用有限
它只能删除游标指向的前一个元素,而且一个 next() 函数之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错
public class Demo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("a");
names.add("b");
names.add("c");
names.add("d");
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.remove();
iterator.remove(); // 报错, 抛出 IllegalStateException 异常
}
}
为什么通过迭代器就能安全的删除集合中的元素呢,我们来看下 remove() 函数是如何实现的,代码如下所示
稍微提醒一下,在 Java 实现中,迭代器类是容器类的内部类,并且 next() 函数不仅将游标后移一位,还会返回当前的元素
public class ArrayList<E> {
transient Object[] elementData;
private int size;
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {
}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size) throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0) throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
}
在上面的代码实现中,迭代器类新增了一个 lastRet 成员变量,用来记录游标指向的前一个元素
通过迭代器去删除这个元素的时候,我们可以更新迭代器中的游标和 lastRet 值,来保证不会因为删除元素而导致某个元素遍历不到
如果通过容器来删除元素,并且希望更新迭代器中的游标值来保证遍历不出错,我们就要维护这个容器都创建了哪些迭代器,每个迭代器是否还在使用等信息,代码实现就变得比较复杂了
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17538550.html