Java面試必備之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源碼的掌握程度。

真正的大師永遠懷著一顆學徒的心

到此這篇關於Java面試必備之ArrayList陷阱解析的文章就介紹到這瞭,更多相關Java ArrayList內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: