背景
昨天小楓接到了一個公司的面試電話,其中一道面試題覺得有點意思,在這里和大家一起分享下,面試題是ArrayList如何洗掉指定元素,乍聽很簡單的問題,但是如果沒有實際踩過坑很容易掉進面試官的陷阱中,我們一起來分析下吧,
問題分析
疑惑滿滿
小楓聽到這個面試題的時候,心想這是什么水面試官,怎么問這么簡單的題目,心想一個for回圈加上equal判斷再洗掉不就完事了嗎?但是轉念一想,不對,這里面肯定有陷阱,不然不會問這么看似簡單的問題,小楓突然想起來之前寫代碼的時候好像遇到過這個問題,也是在ArrayList中洗掉指定元素,但是直接for回圈remove元素的時候還拋出了例外,面試官的陷阱估計在這里,小楓暗自竊喜,找到了面試官埋下的陷阱,
小楓回想起當天的的測驗情況,代碼進行了脫敏改造,當初是要在ArrayList中洗掉指定元素,小楓三下五除二,酣暢淋漓的寫下了如下的代碼,信心滿滿的點了Run代碼的按鈕,結果尷尬了,拋例外了,
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檔案中的內容是怎樣的吧,畢竟class檔案才是JVM真正執行的代碼,不看不知道,一看嚇一跳,JDK原來是這么玩的,原來如此,我們原始代碼中的for-each陳述句,編譯后的實際是以迭代器來代替執行的,
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");
//創建迭代器
Iterator var2 = result.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if ("b".equals(s)) {
result.remove("b");
}
}
}
}
通過ArrayList創建的Itr這個內部類迭代器,于是for-each回圈就轉化成了迭代器加while回圈的方式,原來看上去的for-each回圈被掛羊頭賣狗肉了,
public Iterator<E> iterator() {
return new Itr();
}
Itr這個內部類迭代器,通過判斷hasNext()來判斷迭代器是否有內容,而next()方法則獲取迭代器中的內容,
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];
}
...
}
大致的程序如下所示:

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

我們再來思考下,為什么要有這個檢測呢?這個例外到底起到什么作用呢?我們先來開下ConcurrentModificationException的注釋是怎么描述的,簡單理解就是不允許一個執行緒在修改集合,另一個執行緒在集合基礎之上進行迭代,一旦檢測到了這種情況就會通過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 {
...
}
回顧整個程序

如何正確的洗掉
既然拋例外的原因是回圈使用了迭代器,而洗掉使用ArrayList導致檢測不通過,那么我們就回圈使用迭代器,洗掉也是用迭代器,這樣就可以保證一致了,
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();
}
}
}
總結
本文主要對于ArrayList在for回圈中進行元素洗掉出現的例外進行原始碼分析,這也是面試的時候經常出現的面試陷阱題,面試官通過這樣看似簡單的題目考察候選者的JDK原始碼的掌握程度,
大家好,我是慕楓,感謝各位小伙伴點贊、收藏和評論,文章持續更新,我們下期再見!
微信搜索:慕楓技術筆記,優質文章持續更新,我們有學習打卡的群可以拉你進,一起努力沖擊大廠,另外有很多學習以及面試的材料提供給大家,最近在派送年終福利,趕快來看看吧,
真正的大師永遠懷著一顆學徒的心

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/396190.html
標籤:java
