我们学习了ArrayList和LinkedList容器实现了迭代器,学习了迭代器的原理 实现 设计意图,迭代器模式主要就是解耦容器代码和遍历容器.其实大部分的设计模式都是为了解耦代码

那么,如果在迭代器遍历集合的同时 增加和删除集合中的元素,会发生什么情况呢?会如何应对呢?

使用迭代器来遍历集合元素的同时 增加或者删除元素,会导致这个元素被重复遍历或者遍历不到,不过,并非所有的情况下都会出错,也可能有正常的情况出现,所以无法确定是否真正的出现异常,这种行为被称为 结果不可预期行为或者未决行为,视情况而定

那么为何,在迭代器中进行遍历时候,增加或者删除

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();

names.remove(“a”);

}

}

在执行完成add之后,集合内部有着4个元素,而且在第一次next的时候,指向了b

但是如果这时候remove了a,会导致指向b的指向了c的游标,导致只能遍历到了c,d两个元素,b遍历不到了

图片

但是,如果删除的不是前面的元素 a或者当前指向的元素b,而是c d,就没有什么问题了

也就是视情况而定

在遍历的时候删除元素,会导致某个元素遍历不到,如果添加元素,会怎么样呢?

我们将删除元素改为添加元素

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();

names.add(0, “x”);

}

}

在执行完成add之后,数组中包含了a b c d四个元素,然后next后,游标指向了b,然后第一位处添加了一个元素,导致a b c d都后移了一位,导致元素重新指向了 a,也就是a被遍历了两次

同理,在后面添加元素,不会出现任何的问题,所以添加元素也有着不可预期的行为

图片

如何应对遍历时候改变集合导致的未决行为

当通过迭代器去遍历集合的时候,对于删除和增加这两个未决行为,如果不去限制,是不会出现bug的,也就是一些隐藏很深,很难debug的问题的原因所在,为此我们要解决这个问题,有两种解决方案

(1)遍历的时候不能删除或者修改元素,(2)修改后,再遍历直接报错

第一种实现起来很难,无法去明确的得知开始遍历和结束遍历的事件,如果将创建迭代器的时间作为了开始时间,遍历结束的时间无法去确定,虽然可以将遍历到最后一个元素作为结束点,但是往往遍历并不是每次都遍历到最后一个,我们可能找到了一个值为x的元素后就停止了遍历

那么,或者实现一个结束方法,在每次停止遍历的时候,调用这个结束方法?

这也不靠谱,因为万一又一次没有调用,就麻烦了,而且也增加了开发成本,很容易去漏掉

使用起来可能如下

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();

while (iterator.hasNext()) {

String name = iterator.currentItem();

if (name.equals(“b”)) {

iterator.finishIteration();//主动告知容器这个迭代器用完了

break;

}

}

}

}

所以,我们一般去使用第二种的解决方案,让增删后,遍历出错,Java采用的亦是如此

具体的实现是,在ArrayList中添加一个成员变量 modCount,记录了修改次数,每次修改都会增加1

那么再常见吊带器的时候,就会将这个modCount值传递给这个迭代器中作为一个成员变量,每次调用遍历操作,都会检查是否有别人改变过这个modCount,如果有了改变,就说明了存储的元素已经改变了,要么增加了元素,要么删除了元素,之前的迭代器已经不能正确的运行了,所以直接抛出异常,使用了fail-fast解决方式,抛出了运行时异常

上面的思路这样总结为如下的代码:

public class ArrayIterator implements Iterator {

private int cursor;

private ArrayList arrayList;

private int expectedModCount;

public ArrayIterator(ArrayList arrayList) {

this.cursor = 0;

this.arrayList = arrayList;

this.expectedModCount = arrayList.modCount;

}

@Override

public boolean hasNext() {

checkForComodification();

return cursor < arrayList.size();

}

@Override

public void next() {

checkForComodification();

cursor++;

}

@Override

public Object currentItem() {

checkForComodification();

return arrayList.get(cursor);

}

private void checkForComodification() {

if (arrayList.modCount != expectedModCount)

throw new ConcurrentModificationException();

}

}

//代码示例

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();

names.remove(“a”);

iterator.next();//抛出ConcurrentModificationException异常

}

}

那么,如何在遍历的时候安全的删除一个元素呢?

其实在Java提供的Iterator方法中,还提供了一个remove()方法,可以在遍历集合的时候,删除集合的元素,虽然迭代器理论上只是遍历元素的,将删除放在迭代器中不合适,但是提供了就去看看吧

其实内部实现很简单,就是删除游标指向的前一个元素,而且一个next(),只能对应一个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异常

}

}

现在,看一下,是这么删除的集合的元素的

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成员变量,用于记录游标指向的前一个元素,通过删除这个元素的时候,我们将其重置为了-1,二次删除直接报错,导致只能删除一次

重点回顾

在通过迭代器去遍历集合元素的时候,增加或者删除集合中的元素,可能导致某个元素被重复遍历或者遍历不到,但是并非,所有的情况都会遍历出错,有时候可能也正常遍历,这就是未决行为

而为了避免未决问题,我们常见的就方案就是,让遍历的时候无法进行修改,或者修改后再次遍历,直接报错,我们使用的是第二种解决方案,遍历时候修改过再操作直接报错

课后思考

1.文章给出的Java迭代器的实现代码,如果一个容器对象同时创建了两个迭代器,一个迭代器调用了remove删除了集合中一个元素,那么另一个迭代器是否还可以用?

2.LinkedList底层是基于链表的,进行增删的话,会出现什么不可预期的行为吗?

1.实现中,remove方法中,会在调用集合的remove方法后,将当前的修改量赋值到这个迭代器的内部的修改量属性上,但是对于其他迭代器调用的remove无法感知,自然无法修改本迭代器内部的修改量属性,导致next()会在调用checkForComodification()函数的时候发生报错

2.LinkedList,在增删的时候,由于双向链表的特性,只能感知到上一位和下一位,所以并不会导致异常情况的发生

发表评论

邮箱地址不会被公开。 必填项已用*标注