可惡!簡單的刪除集合中的元素竟然報錯
前言
什么是快速失?。篺ail-fast 機制是java集合(Collection)中的一種錯誤機制。它只能被用來檢測錯誤,因為JDK并不保證fail-fast機制一定會發(fā)生。當多個線程對同一個集合的內(nèi)容進行操作時,就可能會產(chǎn)生fail-fast事件。
運行如下代碼,即可出現(xiàn)異常:
- // 關(guān)于fail-fast的一些思考
- public class FailFastTest {
- public static void main(String[] args) {
- // 構(gòu)建ArrayList
- List<Integer> list = new ArrayList<>();
- list.add(1);
- list.add(2);
- list.add(3);
- list.add(4);
- for (int i : list) {
- list.remove(1);
- }
- }
- }
控制臺會輸出如下異常:
為什么要報這個錯?途中出錯的地方是ArrayList中的代碼,定位到該處代碼:
- final void checkForComodification() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- }
modCount是這個集合修改的次數(shù),這個屬性來自AbstractList,而我們的ArrayList是繼承了該抽象類的。
- protected transient int modCount = 0;
expectedModCount又是啥呢?當我們進行遍歷時候debug一下發(fā)現(xiàn)進行forEach循環(huán)的時候其實走了下面這個方法iterator,而且遍歷這個底層還是走的hasNext方法
- public Iterator<E> iterator() {
- return new Itr();
- }
判斷是否有下一個元素
- public boolean hasNext() {
- return cursor != size;
- }
next()方法用于獲取元素
- 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];
- }
點進這個new Itr(),驚喜的發(fā)現(xiàn)原來這個expectedModCount是在這里被賦值的而且和modCount一樣
- 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; // 注意:此處進行賦值
- ......
- ......
接下來看下ArrayList的remove()方法,其對modCount進行了增加,這是導致報錯的原因
- public E remove(int index) {
- rangeCheck(index);
- modCount++; // 對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;
- }
上面的next()方法這有調(diào)用一個checkForComodification()方法,下面貼一下這方法的代碼
- final void checkForComodification() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- }
ArrayList里面remove()方法進行了modCount++操作,原來是我們對集合進行操作后改變了modCount導致上面代碼成立,從而拋出異常
但是當我們使用Itr類的remove,也就是如下代碼進行對元素改動時,不會拋出ConcurrentModificationException異常
- public void remove() {
- if (lastRet < 0)
- throw new IllegalStateException();
- checkForComodification();
- try {
- ArrayList.this.remove(lastRet);
- cursor = lastRet;
- lastRet = -1;
- // 將ArrayList的modCount賦值給Itr類的expectedModCount
- //這樣再次調(diào)用next方法時就不會出現(xiàn)這倆個值不一致 從而避免報錯
- expectedModCount = modCount;
- } catch (IndexOutOfBoundsException ex) {
- throw new ConcurrentModificationException();
- }
- }
與ArrayList的remove()方法不同的是,該remove()方法調(diào)用ArrayList.this.remove(lastRet);后顯然modCount++了,但是馬上又讓expectedModCount = modCount就是這樣才不會拋出異常。
梳理整個流程:
1、for循環(huán)遍歷實質(zhì)上調(diào)用的是Itr類的方法進行遍歷(Itr類實現(xiàn)了Iterator)
2、Itr類在構(gòu)造的時候會將ArrayList的modCount(實際上modCount是AbstractList的屬性,但是ArrayList繼承了AbstractList)賦值給Itr類的expectedModCount
3、for循環(huán)中調(diào)用的remove()方法時ArrayList的,這個方法會對modCount進行++操作
4、remove方法調(diào)用后,繼續(xù)遍歷會調(diào)用Itr的next()方法,而這個next()方法中的checkForComodification()方法會對modCount和expectedModCount進行對比,由于remove方法已經(jīng)操作過modCount因此這倆個值不會相等,故報錯。
如何改進?
1、可以使用Itr中的remove方法進行改進,改進代碼如下
- public static void main(String[] args) {
- // 構(gòu)建ArrayList
- List<Integer> list = new ArrayList<>();
- list.add(1);
- list.add(2);
- list.add(3);
- list.add(4);
- Iterator<Integer> iterator = list.iterator();
- while(iterator.hasNext()) {
- iterator.next();
- iterator.remove();
- }
- System.out.println(list.size()); // 0
- }
2、使用CopyOnWriterArrayList來代替Arraylist,它對ArrayList的操作時會先復(fù)制一份數(shù)據(jù)出來操作完了再將其更新回去替換掉舊的,所以CopyOnWrite容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實時一致性。這是采用了CopyOnWriterArrayList的fail-safe機制,當集合的結(jié)構(gòu)被改變的時候,fail-safe機制會在復(fù)制原集合的一份數(shù)據(jù)出來,然后在復(fù)制的那份數(shù)據(jù)遍歷,fail-safe機制,在JUC包的集合都是有這種機制實現(xiàn)的。
雖然fail-safe不會拋出異常,但存在以下缺點
1、復(fù)制時需要額外的空間和時間上的開銷。
2、不能保證遍歷的是最新內(nèi)容。
總結(jié)
對于fail-fast機制,我們要操作List集合時可以使用Iterator的remove()方法在遍歷過程中刪除元素,或者使用fail-safe機制的CopyOnWriterArrayList,當然使用的時候需要權(quán)衡下利弊,結(jié)合相關(guān)業(yè)務(wù)場景。