細節決定成敗,移除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()); }
毫無疑問,執行這段程式碼之後,必然報錯,我們看下報錯資訊。
我們可以通過錯誤資訊可以看到,具體的錯誤是在 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 的方式進行查詢,是很便捷的方法。
五、總結
其實這個問題很常見,也是很簡單,但是我們做技術的就是把握細節,通過追溯它的具體實現,發現它的問題所在,這樣你不僅僅知道這樣有問題,而且你還知道這個問題具體是如何產生的,那麼今後不論對於你平時的工作還是面試都是莫大的幫助。