可惡!簡單的刪除集合中的元素竟然報錯
前言
什么是快速失敗:fail-fast 機制是java集合(Collection)中的一種錯誤機制。它只能被用來檢測錯誤,因為JDK并不保證fail-fast機制一定會發生。當多個線程對同一個集合的內容進行操作時,就可能會產生fail-fast事件。
運行如下代碼,即可出現異常:
- // 關于fail-fast的一些思考
- public class FailFastTest {
- public static void main(String[] args) {
- // 構建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是這個集合修改的次數,這個屬性來自AbstractList,而我們的ArrayList是繼承了該抽象類的。
- protected transient int modCount = 0;
expectedModCount又是啥呢?當我們進行遍歷時候debug一下發現進行forEach循環的時候其實走了下面這個方法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(),驚喜的發現原來這個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()方法這有調用一個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
- //這樣再次調用next方法時就不會出現這倆個值不一致 從而避免報錯
- expectedModCount = modCount;
- } catch (IndexOutOfBoundsException ex) {
- throw new ConcurrentModificationException();
- }
- }
與ArrayList的remove()方法不同的是,該remove()方法調用ArrayList.this.remove(lastRet);后顯然modCount++了,但是馬上又讓expectedModCount = modCount就是這樣才不會拋出異常。
梳理整個流程:
1、for循環遍歷實質上調用的是Itr類的方法進行遍歷(Itr類實現了Iterator)
2、Itr類在構造的時候會將ArrayList的modCount(實際上modCount是AbstractList的屬性,但是ArrayList繼承了AbstractList)賦值給Itr類的expectedModCount
3、for循環中調用的remove()方法時ArrayList的,這個方法會對modCount進行++操作
4、remove方法調用后,繼續遍歷會調用Itr的next()方法,而這個next()方法中的checkForComodification()方法會對modCount和expectedModCount進行對比,由于remove方法已經操作過modCount因此這倆個值不會相等,故報錯。
如何改進?
1、可以使用Itr中的remove方法進行改進,改進代碼如下
- public static void main(String[] args) {
- // 構建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的操作時會先復制一份數據出來操作完了再將其更新回去替換掉舊的,所以CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。這是采用了CopyOnWriterArrayList的fail-safe機制,當集合的結構被改變的時候,fail-safe機制會在復制原集合的一份數據出來,然后在復制的那份數據遍歷,fail-safe機制,在JUC包的集合都是有這種機制實現的。
雖然fail-safe不會拋出異常,但存在以下缺點
1、復制時需要額外的空間和時間上的開銷。
2、不能保證遍歷的是最新內容。
總結
對于fail-fast機制,我們要操作List集合時可以使用Iterator的remove()方法在遍歷過程中刪除元素,或者使用fail-safe機制的CopyOnWriterArrayList,當然使用的時候需要權衡下利弊,結合相關業務場景。