問題介紹
List如何在增加元素的同時刪除元素。
這還不簡單?
直接上代碼:
List<String> lists = new ArrayList<>();
lists.add("MRyan");
lists.add("MRyan2");
lists.add("MRyan3");
for (String list : lists) {
if (list.equals("MRyan")) {
lists.remove(list);
}
}
System.out.println(lists);
然後興奮的運行程序,結果發現Exception 。
很慘,報錯了,報瞭如下錯誤:
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:966)
at com.text.text.main(text.java:18)
報錯原因及分析源碼
發生了什麼錯誤?
java.util.ConcurrentModificationException表示ArrayList在迭代的時候如果同時對其進行修改就會拋出的異常。
也就是說這是一個併發的問題。
那爲什麼會發生這個錯誤?
接下來我們來從源碼下手:
定位到ArrayList的源碼
查看ArrayList的add方法:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
首先modCount進行了自增然後調用了自己的private void add(E e, Object[] elementData, int s)方法。
定位到private void add(E e, Object[] elementData, int s)方法
/**
* This helper method split out from add(E) to keep method
* bytecode size under 35 (the -XX:MaxInlineSize default value),
* which helps when add(E) is called in a C1-compiled loop.
*/
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
其實就是首先判斷是否需要擴容,然後將值添加到數組中,數組大小加1。
那你會有疑問了,剛纔出現的modCount是什麼?彆着急往下看
定位到remove(Object o)方法:
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* {@code i} such that
* {@code Objects.equals(o, get(i))}
* (if such an element exists). Returns {@code true} if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return {@code true} if this list contained the specified element
*/
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}
fastRemove(es, i);
return true;
}
最後調用了fastRemove(Object[] es, int i)方法。
定位到fastRemove(Object[] es, int i)
/**
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
我們又發現了modCount進行了自增,於是我們得知modCount其實就是修改的次數,add和remove包括clear,addAll等對list元素的操作都會調用它。
現在回到我們代碼報錯的地方
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:966)
at com.text.text.main(text.java:18)
發現checkForComodification處拋出了異常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
我們發現是因爲判斷modCount和expectedModCount不相等於是拋出了異常。
那麼問題又來了。
expectedModCount是什麼???
/**
* An optimized version of AbstractList.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;
// prevent creating a synthetic constructor
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
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int size = ArrayList.this.size;
int i = cursor;
if (i < size) {
final Object[] es = elementData;
if (i >= es.length)
throw new ConcurrentModificationException();
for (; i < size && modCount == expectedModCount; i++)
action.accept(elementAt(es, i));
// update once at end to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
源碼中發現初始化時expectedModCount=modCount
expectedModCount表示的是ArrayList修改次數的期望值
迭代時調用next方法
@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];
}
也是先調用checkForComodification()方法modCount != expectedModCount則拋出異常。
剛纔我們發現了remove方法會調用fastRemove方法 之後modCount會進行自增,而expectedModCount沒有進行改變,兩者不相等則拋出ConcurrentModificationException()異常。
如何解決此問題
利用Iterator迭代器刪除即可
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
SubList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = SubList.this.modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
因爲Iterator的remove方法會使expectedModCoun和modCount兩值相等,自然沒問題了。
正確代碼:
List<String> lists = new ArrayList<>();
lists.add("MRyan");
lists.add("MRyan2");
lists.add("MRyan3");
Iterator<String> iterator=lists.iterator();
while(iterator.hasNext()){
String list=iterator.next();
if(list.equals("MRyan")){
iterator.remove();
}
}
System.out.println(lists);
運行結果:
[MRyan2, MRyan3]
總結
那就是ArrayList 本身不是線程安全的