不同迭代器实现造成的遍历问题
一、缘起
今天在使用foreach循环遍历LinkedBlockingQueue
的时候,发现一边遍历一遍删除的操作竟然没有出错!代码实现如下:
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.add("a");
queue.add("b");
Iterator<String> iterator1 = queue.iterator();
while (iterator1.hasNext()) {
String s2 = iterator1.next();
System.out.println(s2);
queue.remove(s2);
}
System.out.println(queue);
结果如下:
按照正常集合(List)
遍历删除是会出错的,具体如下:
LinkedList<String> list = new LinkedList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
list.remove(s);
}
System.out.println(list);
结果如下:
这奇奇怪怪的不同激起了我好奇心,于是有了这篇文章…
二、LinkedList迭代器实现
翻阅LinkedList
中的iterator()
方法,它返回了一个Iterator
迭代器类,迭代器类中拥有next()
方法返回下一个集合元素,具体实现如下:
public E next() {
return itr.previous();
}
继续查看previous()
方法
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
哦吼,华生,恭喜你发现了盲点!这个方法中含有一个nextIndex--
,这不由得让我们联想到它的遍历方式是不是通过下标去遍历的,我们再来看看这个nextIndex--
在迭代器类中的初始化代码:
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
看到这,发现nextIndex
确实是保存了下一个节点的下标,当原始集合
发生变化时,迭代器中的下标并没有及时改变,于是便出现了并发同步异常。
三、LinkedBlockingQueue迭代器实现
接下来再来看看Queue
的迭代器类中的next
的实现:
public E next() {
Node<E> p;
if ((p = next) == null)
throw new NoSuchElementException();
lastRet = p;
E x = nextItem;
fullyLock();
try {
E e = null;
for (p = p.next; p != null && (e = p.item) == null; )
p = succ(p);
next = p;
nextItem = e;
} finally {
fullyUnlock();
}
return x;
}
发现它是通过Node
类保存下一个节点位置的,Node
类的实现如下:
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
发现它确实是保存了下一个节点的位置的。
所以无论原始队列再怎么变,自己保存的下一个节点的位置不会改变,也就不会出现List那样的错误。
四、总结
通过数组实现的下标保存并不能保证同步,需要额外的加入同步逻辑,但是通过Node保存下一个节点位置的方法无需同步,更加安全便捷。