Java面試必備之ArrayList陷阱解析
問題分析
疑惑滿滿
小楓聽到這個面試題的時候,心想這是什么水面試官,怎么問這么簡單的題目,心想一個for循環(huán)加上equal判斷再刪除不就完事了嗎?但是轉(zhuǎn)念一想,不對,這里面肯定有陷阱,不然不會問這么看似簡單的問題。小楓突然想起來之前寫代碼的時候好像遇到過這個問題,也是在ArrayList中刪除指定元素,但是直接for循環(huán)remove元素的時候還拋出了異常,面試官的陷阱估計在這里。小楓暗自竊喜,找到了面試官埋下的陷阱。 小楓回想起當(dāng)天的的測試情況,代碼進行了脫敏改造。當(dāng)初是要在ArrayList中刪除指定元素,小楓三下五除二,酣暢淋漓的寫下了如下的代碼,信心滿滿的點了Run代碼的按鈕,結(jié)果尷尬了,拋異常了。
public class TestListMain {
public static void main(String[] args) {
List<String> result = new ArrayList<>();
result.add("a");
result.add("b");
result.add("c");
result.add("d");
for (String s : result) {
if ("b".equals(s)) {
result.remove("b");
}
}
}
}一個大大紅色的異常馬上就出來了,OMG,怎么會這樣呢,感覺代碼沒什么問題啊,趕緊看看拋了什么異常,在哪里拋的異常吧??梢钥闯鰜頀伭艘粋€ConcurrentModificationException的異常,而且是在Itr這個類中的一個檢測方法中拋出來的異常,這是怎么回事呢?我們的原始代碼中并沒有這個Itr代碼,真是百思不得其解。

撥云見日
既然從源代碼分析不出來,我們就看下源代碼編譯后的class文件中的內(nèi)容是怎樣的吧,畢竟class文件才是JVM真正執(zhí)行的代碼,不看不知道,一看嚇一跳,JDK原來是這么玩的。原來如此,我們原始代碼中的for-each語句,編譯后的實際是以迭代器來代替執(zhí)行的。
public class TestListMain {
public TestListMain() {
}
public static void main(String[] args) {
List<String> result = new ArrayList();
result.add("a");
result.add("b");
result.add("c");
result.add("d");
//創(chuàng)建迭代器
Iterator var2 = result.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if ("b".equals(s)) {
result.remove("b");
}
}
}
}通過ArrayList創(chuàng)建的Itr這個內(nèi)部類迭代器,于是for-each循環(huán)就轉(zhuǎn)化成了迭代器加while循環(huán)的方式,原來看上去的for-each循環(huán)被掛羊頭賣狗肉了。
public Iterator<E> iterator() {
return new Itr();
}Itr這個內(nèi)部類迭代器,通過判斷hasNext()來判斷迭代器是否有內(nèi)容,而next()方法則獲取迭代器中的內(nèi)容。
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];
}
...
}大致的過程如下所示:

真正拋異常的地方是這個檢測方法, 當(dāng)modCount與expectedModCount不相等的時候直接拋出異常了。那我們要看下modCount以及expectedModCount分別是什么。這里的modCount代表ArrayList的修改次數(shù),而expectedModCount代表的是迭代器的修改次數(shù),在創(chuàng)建Itr迭代器的時候,將modCount賦值給了expectedModCount,因此在本例中一開始modCount和expectedModCount都是4(添加了四次String元素)。但是在獲取到b元素之后,ArrayList進行了remove操作,因此modCount就累加為5了。因此在進行檢查的時候就出現(xiàn)了不一致,最終導(dǎo)致了異常的產(chǎn)生。到此我們找到了拋異常的原因,循環(huán)使用迭代器進行循環(huán),但是操作元素卻是使用的ArrayList操作,因此迭代器在循環(huán)的時候發(fā)現(xiàn)元素被修改了所以拋出異常。

我們再來思考下,為什么要有這個檢測呢?這個異常到底起到什么作用呢?我們先來開下ConcurrentModificationException的注釋是怎么描述的。簡單理解就是不允許一個線程在修改集合,另一個線程在集合基礎(chǔ)之上進行迭代。一旦檢測到了這種情況就會通過fast-fail機制,拋出異常,防止后面的不可知狀況。
/**
***
* For example, it is not generally permissible for one thread to modify a Collection
* while another thread is iterating over it. In general, the results of the
* iteration are undefined under these circumstances. Some Iterator
* implementations (including those of all the general purpose collection implementations
* provided by the JRE) may choose to throw this exception if this behavior is
* detected. Iterators that do this are known as <i>fail-fast</i> iterators,
* as they fail quickly and cleanly, rather that risking arbitrary,
* non-deterministic behavior at an undetermined time in the future.
***
**/
public class ConcurrentModificationException extends RuntimeException {
...
}回顧整個過程

如何正確的刪除
既然拋異常的原因是循環(huán)使用了迭代器,而刪除使用ArrayList導(dǎo)致檢測不通過。那么我們就循環(huán)使用迭代器,刪除也是用迭代器,這樣就可以保證一致了。
public class TestListMain {
public static void main(String[] args) {
List<String> result = new ArrayList<>();
result.add("a");
result.add("b");
result.add("c");
result.add("d");
Iterator<String> iterator = list.iterator();
while (iterator .hasNext()) {
String str = iterator.next();
if ("b".equals(str)) {
iterator.remove();
}
}
}總結(jié)
本文主要對于ArrayList在for循環(huán)中進行元素刪除出現(xiàn)的異常進行源碼分析,這也是面試的時候經(jīng)常出現(xiàn)的面試陷阱題,面試官通過這樣看似簡單的題目考察候選者的JDK源碼的掌握程度。
真正的大師永遠懷著一顆學(xué)徒的心

到此這篇關(guān)于Java面試必備之ArrayList陷阱解析的文章就介紹到這了,更多相關(guān)Java ArrayList內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解SpringBoot構(gòu)建Docker鏡像的3種方式
這篇文章主要介紹了SpringBoot構(gòu)建Docker鏡像的3種方式,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
IDEA中Spring Initializr沒有Java8選項的解決辦法
在使用IDEA中的Spring Initializr創(chuàng)建新項目時,Java 版本近可選擇Java17,21 ,不能選擇Java8;SpringBoot 版本也只有 3.x,所以本文給大家介紹了IDEA中Spring Initializr沒有Java8選項的解決辦法,需要的朋友可以參考下2024-06-06
Java通過cellstyle屬性設(shè)置Excel單元格常用樣式的全面總結(jié)講解
在處理Excel文件時,經(jīng)常需要對單元格進行樣式設(shè)置,以滿足特定的需求和美化要求,這篇文章主要給大家介紹了關(guān)于Java通過cellstyle屬性設(shè)置Excel單元格常用樣式的相關(guān)資料,需要的朋友可以參考下2024-01-01
Java實現(xiàn)單鏈表反轉(zhuǎn)的多種方法總結(jié)
這篇文章主要給大家介紹了關(guān)于Java實現(xiàn)單鏈表反轉(zhuǎn)的多種方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
詳解Java中的File文件類以及FileDescriptor文件描述類
在Java中File類可以用來新建文件和目錄對象,而FileDescriptor類則被用來表示文件或目錄的可操作性,接下來我們就來詳解Java中的File文件類以及FileDescriptor文件描述類2016-06-06
Java中StringBuilder與StringBuffer使用及源碼解讀
我們前面學(xué)習(xí)的String就屬于不可變字符串,因為理論上一個String字符串一旦定義好,其內(nèi)容就不可再被改變,但實際上,還有另一種可變字符串,包括StringBuilder和StringBuffer兩個類,那可變字符串有什么特點,又怎么使用呢,接下來就請大家跟我一起來學(xué)習(xí)吧2023-05-05
SpringCloud Alibaba Nacos 整合SpringBoot A
這篇文章主要介紹了SpringCloud Alibaba Nacos 整合SpringBoot Admin實戰(zhàn),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
java獲取新insert數(shù)據(jù)自增id的實現(xiàn)方法
這篇文章主要介紹了java獲取新insert數(shù)據(jù)自增id的實現(xiàn)方法,在具體生成id的時候,我們的操作順序一般是:先在主表中插入記錄,然后獲得自動生成的id,以它為基礎(chǔ)插入從表的記錄,需要的朋友可以參考下2019-06-06

