1. 程式人生 > 其它 >細節決定成敗,移除List中的元素,你的姿勢對了嗎?

細節決定成敗,移除List中的元素,你的姿勢對了嗎?

技術標籤:JAVA程式設計師面試java資料結構springjavascript後端

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

一、異常程式碼

我們先看下這段程式碼,你有沒有寫過類似的程式碼

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

毫無疑問,執行這段程式碼之後,必然報錯,我們看下報錯資訊。

細節決定成敗,移除List中的元素,你的姿勢對了嗎?

我們可以通過錯誤資訊可以看到,具體的錯誤是在 checkForComodification 這個方法產生的。

二、ArrayList原始碼分析

首先我們看下 ArrayList 的 iterator 這個方法,通過原始碼可以發現,其實這個返回的是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();
  }
}

引數說明:

cursor : 下一次訪問的索引;

lastRet :上一次訪問的索引;

expectedModCount :對ArrayList修改次數的期望值,初始值為 modCount ;

modCount : 它是 AbstractList 的一個成員變數,表示 ArrayList 的修改次數,通過 add 和remove 方法可以看出;

幾個常用方法:

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。

最後我們看下 ArrayList 的 remove() 方法做了什麼?

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 是為了方便垃圾回收器進行回收。

三、問題定位

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

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() 導致 modCount 與expectedModCount 的值不一致從而引發的問題。

四、解決問題

我們現在知道引發這個問題,是因為兩個變數的值不一致所導致的,那麼有沒有什麼辦法可以解決這個問題呢!答案肯定是有的,通過原始碼可以發現, 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 的方式進行查詢,是很便捷的方法。

五、總結

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