忏悔篇移除List中的元素引发的异常
遍历集合出现的问题
之前遇到对List进行遍历删除的时候,出现来一个ConcurrentModificationException异常,可能好多人都知道list遍历不能直接进行删除操作,但是你可能只是跟我一样知道结果,但是不知道为什么不能删除,或者说这个报错是如何产生的,那么我们今天就来研究一下。一、异常代码
我们先看下这段代码,你有没有写过类似的代码publicstaticvoidmain(String〔〕args){ListIntegerlistnewArrayList();System。out。println(开始添加元素size:list。size());for(inti0;i100;i){list。add(i1);}System。out。println(元素添加结束size:list。size());IteratorIntegeriteratorlist。iterator();while(iterator。hasNext()){Integernextiterator。next();if(next50){list。remove(next);}}System。out。println(执行结束size:list。size());}
毫无疑问,执行这段代码之后,必然报错,我们看下报错信息。
我们可以通过错误信息可以看到,具体的错误是在checkForComodification这个方法产生的。二、ArrayList源码分析
首先我们看下ArrayList的iterator这个方法,通过源码可以发现,其实这个返回的是ArrayList内部类的一个实例对象。publicIteratorEiterator(){returnnewItr();}
我们看下Itr类的全部实现。privateclassItrimplementsIteratorE{intcursor;indexofnextelementtoreturnintlastRet1;indexoflastelementreturned;1ifnosuchintexpectedModCountmodCount;Itr(){}publicbooleanhasNext(){returncursor!size;}SuppressWarnings(unchecked)publicEnext(){checkForComodification();inticursor;if(isize)thrownewNoSuchElementException();Object〔〕elementDataArrayList。this。elementData;if(ielementData。length)thrownewConcurrentModificationException();cursori1;return(E)elementData〔lastReti〕;}publicvoidremove(){if(lastRet0)thrownewIllegalStateException();checkForComodification();try{ArrayList。this。remove(lastRet);cursorlastRet;lastRet1;expectedModCountmodCount;}catch(IndexOutOfBoundsExceptionex){thrownewConcurrentModificationException();}}OverrideSuppressWarnings(unchecked)publicvoidforEachRemaining(Consumerlt;?superEconsumer){Objects。requireNonNull(consumer);finalintsizeArrayList。this。size;inticursor;if(isize){return;}finalObject〔〕elementDataArrayList。this。elementData;if(ielementData。length){thrownewConcurrentModificationException();}while(i!sizemodCountexpectedModCount){consumer。accept((E)elementData〔i〕);}updateonceatendofiterationtoreduceheapwritetrafficcursori;lastReti1;checkForComodification();}finalvoidcheckForComodification(){if(modCount!expectedModCount)thrownewConcurrentModificationException();}}
参数说明:
cursor:下一次访问的索引;
lastRet:上一次访问的索引;
expectedModCount:对ArrayList修改次数的期望值,初始值为modCount;
modCount:它是AbstractList的一个成员变量,表示ArrayList的修改次数,通过add和remove方法可以看出;
几个常用方法:
hasNext():publicbooleanhasNext(){returncursor!size;}
如果下一个访问元素的下标不等于size,那么就表示还有元素可以访问,如果下一个访问的元素下标等于size,那么表示后面已经没有可供访问的元素。因为最后一个元素的下标是size()1,所以当访问下标等于size的时候必定没有元素可供访问。
next():publicEnext(){checkForComodification();inticursor;if(isize)thrownewNoSuchElementException();Object〔〕elementDataArrayList。this。elementData;if(ielementData。length)thrownewConcurrentModificationException();cursori1;return(E)elementData〔lastReti〕;}
注意下,这里面有两个非常重要的地方,cursor初始值是0,获取到元素之后,cursor加1,那么它就是下次索要访问的下标,最后一行,将i赋值给了lastRet这个其实就是上次访问的下标。
此时,cursor变为了1,lastRet变为了0。
最后我们看下ArrayList的remove()方法做了什么?publicbooleanremove(Objecto){if(onull){for(intindex0;indexsize;index)if(elementData〔index〕null){fastRemove(index);returntrue;}}else{for(intindex0;indexsize;index)if(o。equals(elementData〔index〕)){fastRemove(index);returntrue;}}returnfalse;}privatevoidfastRemove(intindex){modCount;intnumMovedsizeindex1;if(numMoved0)System。arraycopy(elementData,index1,elementData,index,numMoved);elementData〔size〕null;cleartoletGCdoitswork}
重点:
我们先记住这里,modCount初始值是0,删除一个元素之后,modCount自增1,接下来就是删除元素,最后一行将引用置为null是为了方便垃圾回收器进行回收。三、问题定位
到这里,其实一个完整的判断、获取、删除已经走完了,此时我们回忆下各个变量的值:
cursor:1(获取了一次元素,默认值0自增了1);
lastRet:0(上一个访问元素的下标值);
expectedModCount:0(初始默认值);
modCount:1(进行了一次remove操作,变成了1);
不知道你还记不记得,next()方法中有两次检查,如果已经忘记的话,建议你往上翻一翻,我们来看下这个判断:finalvoidcheckForComodification(){if(modCount!expectedModCount)thrownewConcurrentModificationException();}
当modCount不等于expectedModCount的时候抛出异常,那么现在我们可以通过上面各变量的值发现,两个变量的值到底是多少,并且知道它们是怎么演变过来的。那么现在我们是不是清楚了ConcurrentModificationException异常产生的愿意呢!
就是因为,list。remove()导致modCount与expectedModCount的值不一致从而引发的问题。四、解决问题
我们现在知道引发这个问题,是因为两个变量的值不一致所导致的,那么有没有什么办法可以解决这个问题呢!答案肯定是有的,通过源码可以发现,Iterator里面也提供了remove方法。publicvoidremove(){if(lastRet0)thrownewIllegalStateException();checkForComodification();try{ArrayList。this。remove(lastRet);cursorlastRet;lastRet1;expectedModCountmodCount;}catch(IndexOutOfBoundsExceptionex){thrownewConcurrentModificationException();}}
你看它做了什么,它将modCount的值赋值给了expectedModCount,那么在调用next()进行检查判断的时候势必不会出现问题。
那么以后如果需要remove的话,千万不要使用list。remove()了,而是使用iterator。remove(),这样其实就不会出现异常了。publicstaticvoidmain(String〔〕args){ListIntegerlistnewArrayList();System。out。println(开始添加元素size:list。size());for(inti0;i100;i){list。add(i1);}System。out。println(元素添加结束size:list。size());IteratorIntegeriteratorlist。iterator();while(iterator。hasNext()){Integernextiterator。next();if(next50){iterator。remove();}}System。out。println(执行结束size:list。size());}
建议:
另外告诉大家,我们在进行测试的时候,如果找不到某个类的实现类,因为有时候一个类有超级多的实现类,但是你不知道它到底调用的是哪个,那么你就通过debug的方式进行查找,是很便捷的方法。五、总结
其实这个问题很常见,也是很简单,但是我们做技术的就是把握细节,通过追溯它的具体实现,发现它的问题所在,这样你不仅仅知道这样有问题,而且你还知道这个问题具体是如何产生的,那么今后不论对于你平时的工作还是面试都是莫大的帮助。