移除List中的元素,你的姿式對了嗎?

以前遇到對List進行遍歷刪除的時候,出現來一個 ConcurrentModificationException 異常,可能好多人都知道list遍歷不能直接進行刪除操做,可是你可能只是跟我同樣知道結果,可是不知道爲何不能刪除,或者說這個報錯是如何產生的,那麼咱們今天就來研究一下。

1、異常代碼

咱們先看下這段代碼,你有沒有寫過相似的代碼程序員

public static void main(String[] args) {

  List<Integer> list = new ArrayList<>();

  System.out.println("開始添加元素 size:" + list.size());

  for (int i = 0; i < 100; i++) {
    list.add(i + 1);
  }

  System.out.println("元素添加結束 size:" + list.size());

  Iterator<Integer> iterator = list.iterator();

  while (iterator.hasNext()) {
    Integer next = iterator.next();
    if (next % 5 == 0) {
      list.remove(next);
    }
  }
  System.out.println("執行結束 size:" + list.size());
}

「毫無疑問,執行這段代碼以後,必然報錯,咱們看下報錯信息。」web

咱們能夠經過錯誤信息能夠看到,具體的錯誤是在checkForComodification 這個方法產生的。面試

2、ArrayList源碼分析

首先咱們看下ArrayListiterator這個方法,經過源碼能夠發現,其實這個返回的是ArrayList內部類的一個實例對象。服務器

public Iterator<E> iterator() {
  return new 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() {}

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

「參數說明:」app

cursor : 下一次訪問的索引;編輯器

lastRet :上一次訪問的索引;ide

expectedModCount :對ArrayList修改次數的指望值,初始值爲modCount源碼分析

modCount :它是AbstractList的一個成員變量,表示ArrayList的修改次數,經過addremove方法能夠看出;測試

「幾個經常使用方法:」

hasNext():

public boolean hasNext() {
 return cursor != size;
}

若是下一個訪問元素的下標不等於size,那麼就表示還有元素能夠訪問,若是下一個訪問的元素下標等於size,那麼表示後面已經沒有可供訪問的元素。由於最後一個元素的下標是size()-1,因此當訪問下標等於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];
}

注意下,這裏面有兩個很是重要的地方,cursor初始值是0,獲取到元素以後,cursor 加1,那麼它就是下次索要訪問的下標,最後一行,將i賦值給了lastRet這個其實就是上次訪問的下標。

此時,cursor變爲了1,lastRet變爲了0。

最後咱們看下ArrayListremove()方法作了什麼?

public boolean remove(Object o) {
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        fastRemove(index);
        return true;
      }
  } else {
    for (int index = 0; index < size; index++)
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}
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初始值是0,刪除一個元素以後,modCount自增1,接下來就是刪除元素,最後一行將引用置爲null是爲了方便垃圾回收器進行回收。

3、問題定位

到這裏,其實一個完整的判斷、獲取、刪除已經走完了,此時咱們回憶下各個變量的值:

cursor : 1(獲取了一次元素,默認值0自增了1);

lastRet :0(上一個訪問元素的下標值);

expectedModCount :0(初始默認值);

modCount :1(進行了一次remove操做,變成了1);

不知道你還記不記得,next()方法中有兩次檢查,若是已經忘記的話,建議你往上翻一翻,咱們來看下這個判斷:

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

modCount不等於expectedModCount的時候拋出異常,那麼如今咱們能夠經過上面各變量的值發現,兩個變量的值究竟是多少,而且知道它們是怎麼演變過來的。那麼如今咱們是否是清楚了ConcurrentModificationException異常產生的願意呢!

「就是由於,list.remove()致使modCountexpectedModCount的值不一致從而引起的問題。」

4、解決問題

咱們如今知道引起這個問題,是由於兩個變量的值不一致所致使的,那麼有沒有什麼辦法能夠解決這個問題呢!答案確定是有的,經過源碼能夠發現,Iterator裏面也提供了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();
  }
}

你看它作了什麼,它將modCount的值賦值給了expectedModCount,那麼在調用next()進行檢查判斷的時候勢必不會出現問題。

那麼之後若是須要remove的話,千萬不要使用list.remove()了,而是使用iterator.remove(),這樣其實就不會出現異常了。

public static void main(String[] args) {

  List<Integer> list = new ArrayList<>();

  System.out.println("開始添加元素 size:" + list.size());

  for (int i = 0; i < 100; i++) {
    list.add(i + 1);
  }

  System.out.println("元素添加結束 size:" + list.size());

  Iterator<Integer> iterator = list.iterator();

  while (iterator.hasNext()) {
    Integer next = iterator.next();
    if (next % 5 == 0) {
      iterator.remove();
    }
  }
  System.out.println("執行結束 size:" + list.size());
}

「建議:」

另外告訴你們,咱們在進行測試的時候,若是找不到某個類的實現類,由於有時候一個類有超級多的實現類,可是你不知道它到底調用的是哪一個,那麼你就經過debug的方式進行查找,是很便捷的方法。

5、總結

其實這個問題很常見,也是很簡單,可是咱們作技術的就是把握細節,經過追溯它的具體實現,發現它的問題所在,這樣你不只僅知道這樣有問題,並且你還知道這個問題具體是如何產生的,那麼從此不論對於你平時的工做仍是面試都是莫大的幫助。

本期分享就到這裏,謝謝各位看到此處,

記得點個贊呦!

日拱一卒,功不唐捐

今日推薦

個人服務器接連被黑客攻擊,我好難
致使MySQL索引失效的幾種常見寫法




本文分享自微信公衆號 - 一個程序員的成長(xiaozaibuluo)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索