面試題:java中為什么foreach中不允許對元素進行add和remove
1、foreach遍歷ArrayList過程中使用 add 和 remove
我們先來看看使用foreach遍歷ArrayList過程中使用 add 和 remove 會出現(xiàn)什么樣子的結(jié)果,然后再分析一下。
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
list.add(i);
}
for (Integer j : list) {
if (j.equals(3)) {
list.remove(3);
}
System.out.println(j);
}
}
運行結(jié)果:
0
1
2
3
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at test.Test.main(Test.java:12)
結(jié)果是出現(xiàn)了ConcurrentModificationException 異常,追蹤下拋出異常的位置(ArrayList.java:911)
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
這個地方告訴我們?nèi)绻?modCount 不等于 expectedModCount 的時候,就會拋出這個異常信息,那么這兩個參數(shù)都代表了什么東西呢?為什么不相等的時候,就會出現(xiàn)異常呢?
2、追根溯源
2.1、modCount是什么?
這時候就要讓我們?nèi)タ丛创a了,在我們點到這個變量的時候,就會有注釋告訴我們了 modCount 是 AbstractList 類中的一個成員變量,該值表示對List的修改次數(shù)。
這時候我們來看看 remove 方法中是否對這個變量進行了增減。

大家可以看到,在 remove 的方法中,實際上只是對 modCount 進行了++,那 expectedModCount 又是個什么東西呢?
2.2、expectedModCount 是什么?
expectedModCount 是 ArrayList 中的一個內(nèi)部類——Itr中的成員變量,我們來看下怎么又扯出個內(nèi)部類Itr。
通過反編譯可以發(fā)現(xiàn)foreach編譯后內(nèi)部是使用迭代器實現(xiàn)的。

迭代器是通過list.iterator()實例化的,list.iterator()就返回了一個內(nèi)部類Itr的對象,從源碼中可以看到Itr實現(xiàn)了Iterator接口,同時聲明了expectedModCount這個成員變量, expectedModCount 表示對ArrayList修改次數(shù)的期望值,它的初始值為 modCount。

2.3、熟悉的checkForComodification方法
從源碼可以看到這個類的next和remove方法里面都調(diào)用了一個checkForComodification方法,看到checkForComodification是不是很熟悉,這不就是異常的拋出位置嗎。

checkForComodification方法是通過判斷modCount和expectedModCount是否相等來決定是否拋出并發(fā)修改異常。

2.4、流程回顧
通過查看編譯后的class文件,可以看出大致流程如下:當j為3時,調(diào)用了remove方法,remove方法中修改了modCount值,然后再輸出j值,再進入下一次循環(huán),此時hasNext為true,進入循環(huán)體第一行代碼,調(diào)用next方法,next方法再調(diào)用checkForComodification方法,然后發(fā)現(xiàn)expectedModCount和modCount不一致,最終拋出ConcurrentModificationException 異常。

也就是說,expectedModCount 初始化為 modCount 了,但是后面 expectedModCount 沒有修改,而在 remove 和 add 的過程中修改了modCount ,這就導致了執(zhí)行的時候,通過 checkForComodification 方法來判斷兩個值是否相等,如果相等了,那么沒問題,如果不相等,那就給你拋出一個異常來。
而這也就是我們通俗說起來的 fail-fast 機制,也就是快速檢測失敗機制。
3、避免fail-fast 機制
3.1、使用listIterator或iterator
fail-fast 機制也是可以避免的,比如再拿出來我們上面的代碼
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i);
}
System.out.println("沒有刪除元素前"+list.toString());
// 迭代器使用listIterator和iterator均可
ListIterator<Integer> listIterator = list.listIterator();
while(listIterator.hasNext()){
Integer integer = listIterator.next();
if(integer==3){
listIterator.remove();
listIterator.add(9);
}
}
System.out.println("刪除元素后"+list.toString());
}
這樣的話,你就發(fā)現(xiàn)是可以運行的,也是沒有問題的,我們看運行結(jié)果:
沒有刪除元素前[0, 1, 2, 3, 4]
刪除元素后[0, 1, 2, 9, 4]
結(jié)果也是顯而易見的,我們實現(xiàn)了在 foreach 中進行 add 和 remove 的操作.
這里有個注意點,迭代器使用listIterator和iterator均可,看源碼可以知道 listIterator其實使用的ListItr內(nèi)部類,ListItr是繼承了Itr類的,同時自己封了一些方法,例如add,hasPrevious,previous等等。所以代碼中的remove方法是Itr類的,add方法是ListItr類的

listIterator和iterator區(qū)別:
- 使用范圍不同,Iterator可以應用于所有的集合,Set、List和Map和這些集合的子類型。而ListIterator只能用于List及其子類型。
- ListIterator有add方法,可以向List中添加對象,而Iterator不能。
- ListIterator和Iterator都有hasNext()和next()方法,可以實現(xiàn)順序向后遍歷,但是ListIterator有hasPrevious()和previous()方法,可以實現(xiàn)逆向(順序向前)遍歷。Iterator不可以。
- ListIterator可以定位當前索引的位置,nextIndex()和previousIndex()可以實現(xiàn)。Iterator沒有此功能。
- 都可實現(xiàn)刪除操作,但是ListIterator可以實現(xiàn)對象的修改,set()方法可以實現(xiàn)。Iterator僅能遍歷,不能修改。
3.2、使用CopyOnWriteArrayList
CopyOnWriteArrayList 這個類也是能解決 fail-fast 的問題的,我們來試一下:
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i);
}
System.out.println("沒有刪除元素前"+list.toString());
for (Integer integer : list) {
if(integer.equals(3)){
list.remove(3);
list.add(9);
}
}
System.out.println("刪除元素后"+list.toString());
}
運行結(jié)果:
沒有刪除元素前[0, 1, 2, 3, 4]
刪除元素后[0, 1, 2, 4, 9]
CopyOnWriteArrayList實現(xiàn)了對這個元素中間進行移除添加的操作,那么他的內(nèi)部源碼是怎么實現(xiàn)的,實際上很簡單,復制
也就是他創(chuàng)建一個新的數(shù)組,再將舊的數(shù)組復制到新的數(shù)組上,但是為什么很少有人推薦這種做法,根本原因還是 復制
因為你使用了復制,那么就一定會出現(xiàn)有兩個存儲相同內(nèi)容的空間,這樣消耗了空間,最后進行 GC 的時候,那是不是也需要一些時間去清理他,所以個人不是很推薦,但是寫出來的必要還是有的。
3.2.1、CopyOnWriteArrayList的add方法
public boolean add(E e) {
// 可重入鎖
final ReentrantLock lock = this.lock;
// 獲取鎖
lock.lock();
try {
// 元素數(shù)組
Object[] elements = getArray();
// 數(shù)組長度
int len = elements.length;
// 復制數(shù)組
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 存放元素e
newElements[len] = e;
// 設(shè)置數(shù)組
setArray(newElements);
return true;
} finally {
// 釋放鎖
lock.unlock();
}
}
處理流程如下:
- 獲取鎖(保證多線程的安全訪問),獲取當前的Object數(shù)組,獲取Object數(shù)組的長度為length,進入步驟②。
- 根據(jù)Object數(shù)組復制一個長度為length+1的Object數(shù)組為newElements(此時,newElements[length]為null),進入下一步驟。
- 將下標為length的數(shù)組元素newElements[length]設(shè)置為元素e,再設(shè)置當前Object[]為newElements,釋放鎖,返回。這樣就完成了元素的添加。
3.2.2、CopyOnWriteArrayList的remove方法
public E remove(int index) {
// 可重入鎖
final ReentrantLock lock = this.lock;
// 獲取鎖
lock.lock();
try {
// 獲取數(shù)組
Object[] elements = getArray();
// 數(shù)組長度
int len = elements.length;
// 獲取舊值
E oldValue = get(elements, index);
// 需要移動的元素個數(shù)
int numMoved = len - index - 1;
if (numMoved == 0) // 移動個數(shù)為0
// 復制后設(shè)置數(shù)組
setArray(Arrays.copyOf(elements, len - 1));
else { // 移動個數(shù)不為0
// 新生數(shù)組
Object[] newElements = new Object[len - 1];
// 復制index索引之前的元素
System.arraycopy(elements, 0, newElements, 0, index);
// 復制index索引之后的元素
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 設(shè)置索引
setArray(newElements);
}
// 返回舊值
return oldValue;
} finally {
// 釋放鎖
lock.unlock();
}
}
處理流程如下:
- 獲取鎖,獲取數(shù)組elements,數(shù)組長度為length,獲取索引的值elements[index],計算需要移動的元素個數(shù)(length - index - 1),若個數(shù)為0,則表示移除的是數(shù)組的最后一個元素,復制elements數(shù)組,復制長度為length-1,然后設(shè)置數(shù)組,進入步驟③;否則,進入步驟②
- 先復制index索引前的元素,再復制index索引后的元素,然后設(shè)置數(shù)組。
- 釋放鎖,返回舊值。
注意
CopyOnWriteArrayList解決 fail-fast 的問題不是通過迭代器來remove或add元素的,而是通過list本身的remove和add方法,所以add的元素位置也不一樣,迭代器是當前位置后面一個,CopyOnWriteArrayList是直接放到最后。
有想法的同學可以看看CopyOnWriteArrayList的listIterator和iterator,其實是一樣的,都是返回的COWIterator內(nèi)部類。

在COWIterator內(nèi)部類中是不支持remove、set、add操作的,至少我使用的jdk1.8是不支持,會直接拋出UnsupportedOperationException異常:

先寫到這兒,后面有空再補充。
到此這篇關(guān)于面試題:java中為什么foreach中不允許對元素進行add和remove的文章就介紹到這了,更多相關(guān)foreach不允許對元素進行add和remove內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Springboot封裝一個自適配的數(shù)據(jù)單位轉(zhuǎn)換工具類
我們在接收前臺傳輸?shù)臄?shù)據(jù)時,往往SpringBoot使用內(nèi)置的數(shù)據(jù)類型轉(zhuǎn)換器把我們提交的數(shù)據(jù)自動封裝成對象等類型,下面這篇文章主要給大家介紹了關(guān)于使用Springboot封裝一個自適配的數(shù)據(jù)單位轉(zhuǎn)換工具類的相關(guān)資料,需要的朋友可以參考下2023-03-03
@RequestBody 部分屬性沒有轉(zhuǎn)化成功的處理
這篇文章主要介紹了@RequestBody 部分屬性沒有轉(zhuǎn)化成功的處理方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
Druid連接池未關(guān)閉導致內(nèi)存泄漏問題
這篇文章主要介紹了Druid連接池未關(guān)閉導致內(nèi)存泄漏問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
java線程池ThreadPoolExecutor的八種拒絕策略示例詳解
ThreadPoolExecutor是一個典型的緩存池化設(shè)計的產(chǎn)物,因為池子有大小,當池子體積不夠承載時,就涉及到拒絕策略。JDK中已預設(shè)了?4?種線程池拒絕策略,下面結(jié)合場景詳細聊聊這些策略的使用場景以及還能擴展哪些拒絕策略2021-11-11
JavaWeb實現(xiàn)同一帳號同一時間只能一個地點登陸(類似QQ登錄的功能)
最近做了企業(yè)項目,其中有這樣的需求要求同一帳號同一時間只能一個地點登陸類似QQ登錄的功能。下面小編通過本文給大家分享實現(xiàn)思路,感興趣的朋友參考下吧2016-11-11
Java 在PDF中添加頁面跳轉(zhuǎn)按鈕功能(代碼演示)
這篇文章主要介紹了Java 在PDF中添加頁面跳轉(zhuǎn)按鈕功能,本文給大家提供了多種功能的按鈕,需要的朋友可以參考下2019-11-11

