项目场景:
iterator下 hasnext() 死循环
循环删除数据异常记录
问题描述
通过循环删除集合中的某条数据时,采用iterator进行删除,但一不小心就会造成死循环,在这记录一下。
以下是错误代码
Iterator<ReadXmlDDVO> iterator = selectDataList.iterator();
while (iterator.hasNext()) {
for (ReadXmlDDVO vo : matchSuccessList) {
if (vo.getInvoiceId().equals(iterator.next().getInvoiceId()) && StringUtil.isNotBlank(vo.getMatchCode())) {
iterator.remove();
}
}
}
以下是正确代码
Iterator<ReadXmlDDVO> iterator = selectDataList.iterator();
while (iterator.hasNext()) {
iterator.next();
for (ReadXmlDDVO vo : matchSuccessList) {
if (vo.getInvoiceId().equals(iterator.next().getInvoiceId()) && StringUtil.isNotBlank(vo.getMatchCode())) {
iterator.remove();
}
}
}
原因分析:
while里没做处理,必须要做iterator.next(); ,这样iterator才能移动,没有移动next永远在第一个,形成了死循环,永远都出不来!
解决方案:
while循环里面添加 iterator.next();
问题延申:
为什么集合里面的删除一定要通过Iterator进行删除呢?
当使用foreach遍历集合时对集合元素进行add / remove时,就会抛出ConcurrentModificationException。
for(String str : list){
if(str.equals("haha")){
list.remove(str); //抛出异常
}
}
// 异常
Exception in thread "main" java.util.ConcurrentModificationException
在对错误分析之前,我们先对foreach进行解语法糖,进行反编译,可以得到以下代码:
Iterator iterator = userNames.iterator();
do
{
if(!iterator.hasNext())
break;
String userName = (String)iterator.next();
if(userName.equals("Jobs"))
userNames.remove(userName);
} while(true);
可以看到,foreach底层实际上是利用iterator和while循环实现的。以ArrayList为例,在调用iterator的时候,会直接返回一个Itr对象,那么我们看一下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对象的几个类成员变量,其中我们看到了一个叫作expectedModCount的字段,那么他是干什么用的呢?我们看下remove函数
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();
}
modCount是ArrayList中的一个成员变量,它表示集合实际被修改的次数,当ArrayList被创建时就存在了,初始值为0。
expectedModCount 是iterator中的一个成员变量,而iterator是ArrayList的一个内部类,当ArrayList调用iterator()方法获取一个迭代器时,会创建一个iterator,并且将expectedModCount 初始化为modCount的值。只有该迭代器修改了集合,expectedModCount 才会修改。
如源代码所示,我们可以看到在Itr进行remove时首先是检查lastRet,这个很合理,就是检查是否越界到最后一个元素。然后进行了checkForComodification检查,具体的操作如上面的函数所示,也就是检查了下modCount是否与expectedModCount是否相等,如果相等,就没事,如果不相等就标出我们上面所出现的异常,那modCount如何变化的呢,我们往下看
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; }
上面是ArrayList的remove函数源码,函数中在每次执行remove时,都会对modCount加一,不仅仅只是在remove时加一,其实add(),clear()函数也会对modCount进行加一操作,那么modCount起什么作用呢,其实他就相当于一个记录ArrayList版本的变量,每对他进行操作时就会将其加一,表示进行了新的操作。
ok,分析了这么多,我们回到最初的问题,为什么用list直接删除元素会报错,而要使用iterator进行删除。
通过源码可以看出,在获取迭代器时,迭代器内的expectedModCount被初始化为modCount,此时如果直接用ArrayList对象直接remove,那么就会改变modCount的值(进行了加一),当迭代器迭代过程中进行checkForComodification检查时,就会发现expectedModCount != modCount,也就是发现当前版本和迭代器记录的版本不一样,那么迭代过程中肯定就会有问题,这时,就会报出之前的异常。
那么,我们再来看下为什么用Itr删除时就可以安全的删除,不会报错呢?在他的remove函数中可以看到下面的一句话,首先其实还是调用了ArrayList的remove函数
在调用完remove函数之后,modCount进行了赋值操作,相当于将最新的版本号告诉了迭代器,所以迭代器在进行异常检查的时候就不会报错,因为他俩是相等的。
还有值得注意的一点是如果你的 Collection / Map 对象实际只有一个元素的时候, ConcurrentModificationException 异常并不会被抛出。这也就是为什么在 javadoc 里面指出: it would be wrong to write a program that depended on this exception for its correctness: ConcurrentModificationException should be used only to detect bugs.