今天在群里聊天時(摸魚)看見一個問題,為什么遍歷List的時候不能remove瑞你?
啥酪惭?你在逗我嗎?憑什么不能remove者甲,我給你remove一個看看春感。
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
run!
Process finished with exit code 0
"for each遍歷"
"..."
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
for (String s: list){
list.remove(s);
}
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at practice.ListTest.main(ListTest.java:14)
遇事不決看源碼
原因
眾所周知,for each的本質(zhì)就是Iterator在next()查詢元素,將java文件編譯后的class文件打開即可看到
List<String> list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
list.remove(s);
}
然后查看ArrayList.java:851源碼,ArrayList的Iterator 在next()最開始之前進行檢查虏缸,同樣的remove方法也會進行checkForComodification()檢查鲫懒。
public E next() {
checkForComodification();
...
}
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();
}
}
然后打開此方法,當(dāng)modCount 不等于expectedModCount的時候就會拋出該異常,那么這個modCount 和expectedModCount又是什么呢刽辙?
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
先看arraylist的add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
然后點進去
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
這里的方法是判斷arraylist在添加元素的時候是否需要擴容窥岩,在ensureExplicitCapacity方法里找到了modCount++,也就是每次添加元素時就會增加宰缤。接下來看expectedModCount 颂翼。
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;
...
}
我們看到,看arralist的Iterator遍歷器的實例變量里慨灭,expectedModCount 就等于modCount朦乏,也就是說在一開始expectedModCount的數(shù)量等于arralist的數(shù)量,這也就說明了氧骤,在第一次Iterator的next方法里并沒有報錯呻疹,因為modCount = expectedModCount,所以錯誤只能出在第二次next方法里语淘,然后接下來看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++; //此時數(shù)量改變了
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
}
看到fastRemove方法里的第一行應(yīng)該就清楚原因了际歼,remove的時候modCount增加了,和一開始的expectedModCount 姑蓝,也就是arraylist的一開始的數(shù)量不一致了鹅心,所以會導(dǎo)致ConcurrentModificationException。
所以正確的用法是什么纺荧?
正確用法
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
String s = (String)iterator.next();
//if(...)
iterator.remove();
}
why?
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();
}
}
延伸
那么有沒有一種list旭愧,能直接就在遍歷的時候直接進行刪除呢?
答案肯定是有的(聽大佬說的)
CopyOnWriteArrayList
不信宙暇?試試
List<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
for(String s : list){
list.remove(s);
}
Process finished with exit code 0
CopyOnWriteArrayList如何做到的输枯?
CopyOnWriteArrayList 類的所有可變操作(add,set等等)都是通過創(chuàng)建底層數(shù)組的新副本來實現(xiàn)的占贫。當(dāng) List 需要被修改的時候桃熄,并不直接修改原有數(shù)組對象,而是對原有數(shù)據(jù)進行一次拷貝型奥,將修改的內(nèi)容寫入副本中瞳收。寫完之后,再將修改完的副本替換成原來的數(shù)據(jù)厢汹,這樣就可以保證寫操作不會影響讀操作了螟深。
從 CopyOnWriteArrayList 的名字可以看出,CopyOnWriteArrayList 是滿足 CopyOnWrite 的 ArrayList烫葬,所謂 CopyOnWrite 的意思:界弧、就是對一塊內(nèi)存進行修改時,不直接在原有內(nèi)存塊中進行寫操作搭综,而是將內(nèi)存拷貝一份垢箕,在新的內(nèi)存中進行寫操作,寫完之后设凹,再將原來指向的內(nèi)存指針指到新的內(nèi)存舰讹,原來的內(nèi)存就可以被回收。
看看它的add方法
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//獲取當(dāng)前已有的數(shù)組
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//拷貝一份新的數(shù)組
newElements[len] = e;//將增加的值放入新的數(shù)組
setArray(newElements);//把當(dāng)前數(shù)組對象設(shè)置為剛剛拷貝的數(shù)組值
return true;
} finally {
lock.unlock();
}
}
remove(object)方法
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length);//找到當(dāng)前元素的下標(biāo)索引值
return (index < 0) ? false : remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock();//加鎖
try {
Object[] current = getArray();//再次獲取當(dāng)前數(shù)組
int len = current.length;
if (snapshot != current) findIndex: {//查找需要移除的元素在數(shù)組里的索引
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
if (index >= len)
return false;
if (current[index] == o)
break findIndex;
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
Object[] newElements = new Object[len - 1];//創(chuàng)建一個新的數(shù)組闪朱,拷貝
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);//設(shè)置拷貝后的數(shù)組
return true;
} finally {
lock.unlock();//釋放鎖
}
}
從add方法和remove方法里不難看出月匣,不管是添加元素還是移除元素,都是通過拷貝數(shù)組并重新賦值來實現(xiàn)的奋姿,所以在遍歷時锄开,remove或者add或者其他一些列操作都不會引起和arraylist一樣的異常的,甚至称诗,你在使用Iterator的時候萍悴,它還會報錯。
private static class COWSubListIterator<E> implements ListIterator<E> {
...
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
...
}
List<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
var2.remove();
}
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1176)
at practice.ListTest.main(ListTest.java:19)
總結(jié)
1.ArrayList在foreach的時候不能直接使用list.remove來操作數(shù)組,因為ArrayList的Iterator 的next方法里每次都會判斷當(dāng)前的數(shù)組的數(shù)量是否和修改后的數(shù)量是否對等癣诱,也就是expectedModCount 和modCount计维,而list.remove方法會修modCount的數(shù)量,所以下一次判斷時就不對等,就會報錯撕予。
2.CopyOnWriteArrayList 可以實現(xiàn)遍歷時直接list.remove,因為CopyOnWriteArrayList 的增刪是通過每次都拷貝一次數(shù)組重新賦值實現(xiàn)的实抡。
3.這個算是java的基礎(chǔ),我居然都不知道吆寨。
4.我是不可回收垃圾赏淌。