Java面試-容器的遍歷


當我們用增強for循環遍歷非並發容器(HashMap、ArrayList等),如果修改其結構,會拋出異常ConcurrentModificationException,因此在阿里巴巴的Java規范中有說到:不要在foreach循環里進行元素的remove/add操作,remove元素請使用Iterator方式。,但是不是真的就不可以在增強for循環中修改結構嗎?其原理又是什么呢?

ConcurrentModificationException的含義

ConcurrentModificationException可以將其通俗的翻譯為並發修改異常,那么關注點就在並發修改了。也許有些人會說,我只是在單線程中修改了,並沒有並發操作,但系統也拋了這樣的這樣的錯誤,這是為什么呢?別急,我們看看它的源碼解釋:

This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.

這個異常就是應用程序在做一些系統不允許的操作時拋出的。記住,只要是系統不允許的操作,就一定會拋錯的。

后面有一個值得注意的地方

Note that fail-fast behavior cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast operations throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: ConcurrentModificationException should be used only to detect bugs.

fail-fast(快速失敗)並不能一定被保證,所以fail-fast操作會盡最大努力拋出該異常。既然是盡最大努力,因此無論是不是並發操作,只要是修改了,就一定會報錯。

既然如此,我們來看看for循環中遍歷修改容器結構,系統是如何知道的。

增加for循環的原理

我們來看看增強for循環遍歷修改HashMap的代碼:

    Map<String, String> hashMap = new HashMap<>(10);
    // 添加
    for (int i = 0; i < 10; i++) {
      hashMap.put("key" + i, "value" + i);
    }
    // 遍歷修改
    for (Entry<String, String> entry : hashMap.entrySet()) {
      String key = entry.getKey();
      hashMap.remove(key);
    }

這個時候,你如果運行的話,就會拋出ConcurrentModificationException,這個時候我們需要具體調試一下,發現遍歷第一次並刪除時沒有報錯,但第二次遍歷,在for循環的括號執行完后,就拋出了異常,這又是為什么呢?

讓我們反編譯一下class文件,看看究竟增強for循環做了什么:

    Map<String, String> hashMap = new HashMap(10);

    for(int i = 0; i < 10; ++i) {
      hashMap.put("key" + i, "value" + i);
    }

    Iterator var5 = hashMap.entrySet().iterator();
    while(var5.hasNext()) {
      Entry<String, String> entry = (Entry)var5.next();
      String key = (String)entry.getKey();
      hashMap.remove(key);
    }

我們發現,雖然寫法上是增強for循環,但實際還是使用的while結合iterator進行遍歷,現在我們貼上這個代碼進行調試。

發現在第二次var5.next()處拋異常,接下來我們看看next方法究竟做了什么?

HashMap的源碼中顯示:

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

我們注意到,nextNode()方法的第一個判斷就決定了是否拋出ConcurrentModificationException,那么modCountexpectedModCount究竟是什么呢?

modCount和expectedModCount

我們來看看modCountexpectedModCount的關系,當我們調用Iterator var5 = hashMap.entrySet().iterator();時,源代碼做了什么:

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

在一開始,就讓expectedModCount等於modCount,而當我們調用hashMap.remove(key);時,實際上修改了modCount的值:

    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

modCount增大1,那么,當我們下一次調用var5.next()時,自然就發現modCountexpectedModCount不等了。

修改結構的正確姿勢

使用增強for循環,本質還是在使用iterator,那為什么大家都在推介使用iterator.remove()呢?讓我們看看源代碼:

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }

我們發現,這個remove方法雖然也調用了removeNode,但它在最后一步再次將modCount的值賦給expectedModCount,因此保證了下一次調用next()方法是不拋錯。

所以,我們要么就直接顯示地使用iterator,用它的remove方法移除對象。如果你實在想用增強for循環遍歷刪除,那么也只能在刪除一個后,立刻退出循環。但無論用哪種方法,當多個線程同時修改時,都會有出錯的可能性,因為你即時保證單個線程內的modCountexpectedModCount,但這個操作並不能保證原子性。

總結

如果在多線程環境下,我更推介使用ConcurrentHashMap,因為它沒有modCountexpectedModCount的概念,因此,即時你是使用增強for循環遍歷刪除,也不會出現問題。

有興趣的話可以關注我的公眾號或者頭條號,說不定會有意外的驚喜。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM