遍歷list時刪除元素髮生了什麼?

導語:

最近寫了一個bug就是在遍歷list的時候刪除了裏面一個元素,其實以前看過阿里的java開發規範,知道在遍歷的時候刪除元素會產生問題,可是寫的快的時候仍是會沒注意到,那正好研究下里面的機制看。咱們看看阿里規範怎麼寫的:
java

首先提出一個概念:fail-fast
摘自百度百科:
fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操做時,就可能會產生fail-fast事件。
例如:當某一個線程A經過iterator去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變了;那麼線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。
簡單來講是java爲了防止出現併發異常的一個機制,可是其實在單線程下也能夠產生。多線程

實例分析:

接下來,我會經過6個例子來探究下,遍歷list時刪除元素會發生什麼,和它的源碼探究。
前提先造了一個list:併發

private List<String> list = new ArrayList<String>() {{
       add("元素1");
       add("元素2");
       add("元素3");
   }};

1.普通的for循環

public void test1() {
       for (int i = 0; i < list.size() - 1; i++) {
           if ("元素3".equals(list.get(i))) {
               System.out.println("找到元素3了");
           }

           if ("元素2".equals(list.get(i))) {
               list.remove(i);
           }
       }
   }
   //  這裏不會輸出找到元素3 由於遍歷到元素2的時候刪除了元素2 list的size變小了
   //  因此就產生問題了

2.for循環另外一種狀況

public void test2() {
       for (int i = 0; i < list.size() - 1; i++) {
           if ("元素2".equals(list.get(i))) {
               list.remove(i);
           }

           if ("元素3".equals(list.get(i))) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 這裏會輸出元素3 可是實際上是在遍歷到元素2的時候輸出的
   // 遍歷到元素2 而後刪除 到了判斷元素3的條件的時候i是比原來小了1
   // 因此又陰差陽錯的輸出了正確結果

3.加強for循環

public void test3() {
       for (String item : list) {
           if ("元素2".equals(item)) {
               list.remove(item);
           }

           if ("元素3".equals(item)) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 這裏和上面的結果有點不同 可是仍是沒有輸出元素3的打印語句
   // 這裏反編譯下java文件就能夠知道是爲啥啦

   public void test3() {
         Iterator var1 = this.list.iterator();

         while(var1.hasNext()) {
           // 爲了顯示區別這裏
             String var2 = (String)var1.next();
             if ("元素2".equals(var2)) {
                 this.list.remove(var2);
             }

             if ("元素3".equals(var2)) {
                 System.out.println("找到元素3了");
             }
         }

     }

反編譯的文件能夠知道這裏加強for反編譯是用了迭代器,來判斷是否還有元素,而後remove用的是list的方法。
咱們看下ArrayList中的hasNext(),next()是怎麼寫的。下面是ArrayList中的內部類Itr實現了Iterator接口,ide

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() {}

        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();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

這裏cursor在next()執行時,cursor = i + 1,因此當運行到"元素2".equal(item)這裏,元素2被移除,當遍歷當元素3的時候size = 2.cursor = i + 1 = 1 + 1 也是2.
hasNext()中cursor == size就直接退出了。ui

4.foreach

public void test4() {
        list.forEach(
                item -> {
                    if ("元素2".equals(item)) {
                        list.remove(item);
                    }

                    if ("元素3".equals(item)) {
                        System.out.println("找到元素3了");
                    }
                }
        );
    }
    // 這裏拋出了咱們期待已經的fail-fast java.util.ConcurrentModificationException

點進去看下ArrayList的源碼this

@Override
   public void forEach(Consumer<? super E> action) {
       Objects.requireNonNull(action);
       final int expectedModCount = modCount;
       @SuppressWarnings("unchecked")
       final E[] elementData = (E[]) this.elementData;
       final int size = this.size;
       for (int i=0; modCount == expectedModCount && i < size; i++) {
           action.accept(elementData[i]);
       }
       if (modCount != expectedModCount) {
           throw new ConcurrentModificationException();
       }
   }

根據報錯信息咱們能夠知道是
if (modCount != expectedModCount) {spa

throw new ConcurrentModificationException();

}
拋出的異常,那麼這個modCount元素是從哪裏來的?
咱們找到AbstractList,這個是ArrayList的父類。
看看源碼裏面是怎麼說的線程

/**
    * The number of times this list has been <i>structurally modified</i>.
    * Structural modifications are those that change the size of the
    * list, or otherwise perturb it in such a fashion that iterations in
    * progress may yield incorrect results.
    *
    * <p>This field is used by the iterator and list iterator implementation
    * returned by the {@code iterator} and {@code listIterator} methods.
    * If the value of this field changes unexpectedly, the iterator (or list
    * iterator) will throw a {@code ConcurrentModificationException} in
    * response to the {@code next}, {@code remove}, {@code previous},
    * {@code set} or {@code add} operations.  This provides
    * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
    * the face of concurrent modification during iteration.
    *
    * <p><b>Use of this field by subclasses is optional.</b> If a subclass
    * wishes to provide fail-fast iterators (and list iterators), then it
    * merely has to increment this field in its {@code add(int, E)} and
    * {@code remove(int)} methods (and any other methods that it overrides
    * that result in structural modifications to the list).  A single call to
    * {@code add(int, E)} or {@code remove(int)} must add no more than
    * one to this field, or the iterators (and list iterators) will throw
    * bogus {@code ConcurrentModificationExceptions}.  If an implementation
    * does not wish to provide fail-fast iterators, this field may be
    * ignored.
    */
protected transient int modCount = 0;

英文好的同窗能夠本身看下,英文很差的同窗能夠默默打開谷歌翻譯或者有道詞典了。翻譯

剩下的同窗能夠聽一下個人理解,其實只看第一段基本上就知道它是幹嗎的了。意思是這個字段是記錄了list的結構性修改次數(咱們能夠理解爲add,remove這些會改變list大小的操做)。若是是其餘方式致使的就回拋出ConcurrentModificationException異常。
因此咱們再去看看子類的ArrayList的add,remove方法。code

// 這個是add裏面的子方法
private void ensureExplicitCapacity(int minCapacity) {
      modCount++;

      // overflow-conscious code
      if (minCapacity - elementData.length > 0)
          grow(minCapacity);
  }

// 這個是remove(int index)
  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;
    }
// 這是remove(Object o)
private void fastRemove(int index) {
       modCount++;
       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
   }

看完這幾個方法,是否是都看到了modCount++這個操做?是的,添加和刪除的時候都會對這個modCount加一。好了,咱們在回頭看看爲啥test4()會拋出異常。
首先咱們知道list裏面add了三個元素因此modCount在添加完元素的時候是3,而後它開始遍歷,當發現元素2的時候,去移除了元素2,此時modCount變爲4.
咱們在會到ArrayList的

@Override
   public void forEach(Consumer<? super E> action) {
       Objects.requireNonNull(action);
       final int expectedModCount = modCount;
       @SuppressWarnings("unchecked")
       final E[] elementData = (E[]) this.elementData;
       final int size = this.size;
       for (int i=0; modCount == expectedModCount && i < size; i++) {
           action.accept(elementData[i]);
       }
       if (modCount != expectedModCount) {
           throw new ConcurrentModificationException();
       }
   }

能夠看到一開始把modCoun賦值給了expectedModCount,而後for循環裏面和最後的if條件均對這個modCount有跑斷,if裏面若是發現modCount和expectedModCount不相等了,就拋出異常。當遍歷到元素2,action.accept(elementData[i]);這一行執行完,再去遍歷元素3的時候由於modCount == expectedModCount不相等了,因此循環推出,所以不會打印出找到元素3了,而且執行到if條件直接拋出異常。

5.迭代器

咱們在來看看正確的使用方式,使用迭代器

public void test5() {
       Iterator<String> iterator = list.iterator();
       while (iterator.hasNext()) {
           String temp = iterator.next();
           if ("元素2".equals(temp)) {
               iterator.remove();
           }

           if ("元素3".equals(temp)) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 這裏打印出了 找到元素3了

上面的test3咱們已經看到這個迭代器的代碼了,那爲啥它沒問題呢?其實原理很是的沙雕,不信你看!

這是迭代器的remove方法

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
        // 看到沒有直接把modeCount從新賦值給了expectedModCount 因此它們一直會相等啊
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

因此它是能夠找到元素3的,由於直接忽略了remove對modCount的影響。

6.removeIf

jdk8還出了一種新寫法,封裝了在遍歷元素時刪除元素,removeIf(),以下:

list.removeIf(item -> "元素2".equals(item)

點進去能夠看到代碼和上一個差很少只是封裝了一層罷了。

default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

總結:

1.咱們探究了好幾種遍歷時刪除元素的方式,也知道了fail-fast的基本概念。 2.學會了之後遍歷的時候刪除元素要使用迭代器哦,並且發現即便是單線程依然能夠拋出ConcurrentModificationException 3.若是是多線程操做list的話建議使用CopyOnWriteArrayList,或者對迭代器加鎖也行,看我的習慣吧~

相關文章
相關標籤/搜索