遍历 List 是我们在写代码中经常碰到的,有时候我们会碰到想在遍历的途中删掉某些元素的需求,但一不小心可能就会报快速失败错误(FastFail),这其实跟 List 当中的一些实现有关,下面我选取了两种情况,来自知乎大神的回答,加上自己的疑惑和总结。
作者:RednaxelaFX
链接:https://www.zhihu.com/question/56586732/answer/149650876
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
总结下就是两种情况:一种是可以删除末尾的元素,另一种是在倒数第二时可以删除任何一个元素。
我觉得题主的首要问题是被自己写的代码(以及 JDK 的 ArrayList 那可恶的 API)给坑了。
ArrayList 上有两个版本的 remove 方法:
public E remove(int index)
public boolean remove(Object o)
题主很可能以为自己调用的是第二个版本,但实际上调用的是第一个版本——remove(3) 删除了位于末尾的元素,而不是位于倒数第二的元素。
ArrayList.iterator() 返回出来的 Iterator,里面的 hasNext()是不关心 modification count 的,而 next()会去检查 modification count:
List 中的内部类
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
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();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
所以题主的那个程序实际做的事情就是:
-
通过 ArrayList.iterator() 得到了一个新的 iterator,开始遍历
-
list.remove(3):删除了位于 index 3 的元素(Integer.valueOf(4) 得到的对象)
-
然后调用 iterator.hasNext(),得到 false,于是就退出了循环而没有去执行那个会检查 modification count 的 next()方法。
ArrayList 的 JavaDoc 说:
The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
没有特别指定 Iterator 里的哪些方法一定会根据 fail-fast 原则而抛异常。但 Iterator.hasNext()的 JavaDoc 说:
hasNext
boolean hasNext()
Returns true if the iteration has more elements. (In other words, returns true if next() would return an element rather than throwing an exception.)
Returns:true if the iteration has more elements
就这个规定来说,我觉得题主观察到的现象应该算是 JDK 实现的上的巧合:因为如果在这个位置调用 next()的话会抛 ConcurrentModificationException 异常,所以 hasNext()也要返回 false,于是就好啦。
但 JDK 这个具体实现看起来还是有 bug,应该让 hasNext()也做 checkForComodification()的不抛异常对应动作才对。
就这样。
还是未能解决下面的疑问:
在判断的是倒数第二个的时候,所有的元素都能删。
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.add("f");
Iterator it = list.iterator();
while (it.hasNext()) {
String itt = (String) it.next();
if (itt.equals("e")) {
list.remove("a");
}
}
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
下面是解释:
作者:xRay
链接:https://www.zhihu.com/question/56916067/answer/151995061
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
为什么没抛异常,跟下代码其实就一清二楚了。
ArralyList 有个版本号 modCount,每次修改 ArrayList 时版本号就会往上加。Iterator 里面也有个版本号 expectedModCount 它的初始值就是 modCount。只有 expectedModCount != modCount 才会抛异常。所以 printList 里面的绝对不会抛异常。那问题就出在进行 remove 的那个迭代器上,首先迭代器是在什么时候比较这两个值呢? 答案是在 remove 跟 next 的时候,也就是说 hasNext 它不会抛出这个异常。
List 中的删除方法:
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
然后,看下题主的这段代码
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
Integer integer = iterator.next();
if(integer == 2)
list.remove(integer);
}
这里面调用了 List 的 remove 方法,所以它不会抛出 ConcurrentModificationException。问题是 remove 之后为什么 hasNext 会返回 false。我们看下 hasNext 方法
int cursor; // index of next element to return
public boolean hasNext() { return cursor != size; }
看注释,cursor 是指向一个元素的,也就是当 list.remove(integer=2)的时候,cursor 实际等于 2。而 list.remove(integer)后 size=3-1=2,所以 hasNext 返回 false,也就不会执行 next 方法,所以也就不会抛出 ConcurrentModificationException。
不过,这个问题,更有意思的 ArrayList 有两个 remove 方法
public E remove(int index) boolean remove(Object o)
当参数是 Integer 时会调用哪个? 我在 JLS 中找到这么一段,意思是选择重载函数时不会优先考虑装箱跟拆箱
The first phase (§15.12.2.2) performs overload resolution without permitting boxing or unboxing conversion, or the use of variable arity method invocation. If no applicable method is found during this phase then processing continues to the second phase.
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于