【原理探究】女友問我ArrayList遍歷時刪除元素的正確姿式是什麼?

立刻就到跳槽季了,建了一個面試題解答的項目,你們能夠看一看,若是對你們有幫助,但願你們幫忙給一個star,謝謝各位大佬了!java

《面試指北》項目地址:github.com/NotFound9/i…git

簡介

咱們在項目開發過程當中,常常會有需求須要刪除ArrayList中的某個元素,而使用不正確的刪除方式,就有可能拋出異常。或者在面試中,會遇到面試官詢問遍歷時如何正常刪除元素。因此在本篇文章中,咱們會對幾種刪除元素的方式進行測試,並對原理進行研究,但願能夠幫助到你們!github

ArrayList遍歷時刪除元素的幾種姿式

首先結論以下:面試

第1種方法 - 普通for循環正序刪除(結果:會漏掉元素判斷)api

第2種方法 - 普通for循環倒序刪除(結果:正確刪除)數組

第3種方法 - for-each循環刪除(結果:拋出異常)bash

第4種方法 - Iterator遍歷,使用ArrayList.remove()刪除元素(結果:拋出異常)併發

第5種方法 - Iterator遍歷,使用Iterator的remove刪除元素(結果:正確刪除)ide

下面讓咱們來詳細探究一下緣由吧!post

首先初始化一個數組arrayList,假設咱們要刪除等於3的元素。

public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        arrayList.add(3);
        arrayList.add(4);
        arrayList.add(5);
        removeWayOne(arrayList);
    }
複製代碼

第1種方法 - 普通for循環正序刪除(結果:會漏掉元素判斷)

for (int i = 0; i < arrayList.size(); i++) {
	if (arrayList.get(i) == 3) {//3是要刪除的元素
		arrayList.remove(i);
		//解決方案: 加一行代碼i = i - 1; 刪除元素後,下標減1
	}
    System.out.println("當前arrayList是"+arrayList.toString());
}
//原ArrayList是[1, 2, 3, 3, 4, 5]
//刪除後是[1, 2, 3, 4, 5]
複製代碼

輸出結果:

當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 4, 5]
當前arrayList是[1, 2, 3, 4, 5]
當前arrayList是[1, 2, 3, 4, 5]
複製代碼

能夠看到少刪除了一個3,

緣由在於調用remove刪除元素時,remove方法調用System.arraycopy()方法將後面的元素移動到前面的位置,也就是第二個3會移動到數組下標爲2的位置,而在下一次循環時,i+1以後,i會爲3,不會對數組下標爲2這個位置進行判斷,因此這種寫法,在刪除元素時,被刪除元素a的後一個元素b會移動a的位置,而i已經加1,會忽略對元素b的判斷,因此若是是連續的重複元素,會致使少刪除。

解決方案

能夠在刪除元素後,執行i=i-1,使得下次循環時再次對該數組下標進行判斷。

第2種方法 - 普通for循環倒序刪除(結果:正確刪除)

for (int i = arrayList.size() -1 ; i>=0; i--) {
    if (arrayList.get(i).equals(3)) {
        arrayList.remove(i);
    }
    System.out.println("當前arrayList是"+arrayList.toString());
}
複製代碼

輸出結果:

當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 4, 5]
當前arrayList是[1, 2, 4, 5]
當前arrayList是[1, 2, 4, 5]
當前arrayList是[1, 2, 4, 5]
複製代碼

這種方法能夠正確刪除元素,由於調用remove刪除元素時,remove方法調用System.arraycopy()將被刪除元素a後面的元素向前移動,而不會影響元素a以前的元素,因此倒序遍歷能夠正常刪除元素。

第3種方法 - for-each循環刪除(結果:拋出異常)

public static void removeWayThree(ArrayList<Integer> arrayList) {
    for (Integer value : arrayList) {
        if (value.equals(3)) {//3是要刪除的元素
            arrayList.remove(value);
        }
    System.out.println("當前arrayList是"+arrayList.toString());
    }
}
複製代碼

輸出結果:

當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 4, 5]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.test.ArrayListTest1.removeWayThree(ArrayListTest1.java:50)
	at com.test.ArrayListTest1.main(ArrayListTest1.java:24)
複製代碼

會拋出ConcurrentModificationException異常,主要在於for-each的底層實現是使用ArrayList.iterator的hasNext()方法和next()方法實現的,咱們可使用反編譯進行驗證,對包含上面的方法的類使用如下命令反編譯驗證

javac ArrayTest.java//生成ArrayTest.class文件
javap -c ArrayListTest.class//對class文件反編譯
複製代碼

獲得removeWayThree方法的反編譯代碼以下:

public static void removeWayThree(java.util.ArrayList<java.lang.Integer>);
    Code:
       0: aload_0
       1: invokevirtual #12   // Method java/util/ArrayList.iterator:()Ljava/util/Iterator;
       4: astore_1
       5: aload_1
       6: invokeinterface #13,  1 // InterfaceMethod java/util/Iterator.hasNext:()Z   調用Iterator.hasNext()方法
      11: ifeq          44
      14: aload_1
      15: invokeinterface #14,  1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;調用Iterator.next()方法
      20: checkcast     #9                  // class java/lang/Integer
      23: astore_2
      24: aload_2
      25: iconst_3
      26: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      29: invokevirtual #10                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z 
      32: ifeq          41
      35: aload_0
      36: aload_2
      37: invokevirtual #15                 // Method java/util/ArrayList.remove:(Ljava/lang/Object;)Z
      40: pop
      41: goto          5
      44: return
複製代碼

能夠很清楚得看到Iterator.hasNext()來判斷是否還有下一個元素,和Iterator.next()方法來獲取下一個元素。而由於在刪除元素時,remove()方法會調用fastRemove()方法,其中會對modCount+1,表明對數組進行了修改,將修改次數+1。

public boolean remove(Object o) {
     if (o == null) {
         for (int index = 0; index < size; index++)
             if (elementData[index] == null) {
                 fastRemove(index);
             return true;
         }
     } else {
         for (int index = 0; index < size; index++)
             if (o.equals(elementData[index])) {
                 fastRemove(index);
                 return true;
             }
     }
 		return false;
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
    			System.arraycopy(elementData, index+1, elementData, index,numMoved);
    elementData[--size] = null; // clear to let GC do its work
}


複製代碼

而當刪除完元素後,進行下一次循環時,會調用下面源碼中Itr.next()方法獲取下一個元素,會調用checkForComodification()方法對ArrayList進行校驗,判斷在遍歷ArrayList是否已經被修改,因爲以前對modCount+1,而expectedModCount仍是初始化時ArrayList.Itr對象時賦的值,因此會不相等,而後拋出ConcurrentModificationException異常。

那麼有什麼辦法可讓expectedModCount及時更新呢?

能夠看到下面Itr的源碼中,在Itr.remove()方法中刪除元素後會對 expectedModCount更新,因此咱們在使用刪除元素時使用Itr.remove()方法來刪除元素就能夠保證expectedModCount的更新了,具體看第5種方法。

private class Itr implements Iterator<E> {
        int cursor;       // 遊標
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;//期待的modCount值

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();//判斷expectedModCount與當前的modCount是否一致
            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];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;//更新expectedModCount
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
複製代碼

第4種方法 - Iterator遍歷,使用ArrayList.remove()刪除元素(結果:拋出異常)

Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
    Integer value = iterator.next();
    if (value.equals(3)) {//3是要刪除的元素
    		arrayList.remove(value);
    }
    System.out.println("當前arrayList是"+arrayList.toString());
}
複製代碼

輸出結果:

當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 4, 5]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.test.ArrayListTest1.removeWayFour(ArrayListTest1.java:61)
	at com.test.ArrayListTest1.main(ArrayListTest1.java:25)
複製代碼

第3種方法在編譯後的代碼,實際上是跟第4種是同樣的,因此第四種寫法也會拋出ConcurrentModificationException異常。這種須要注意的是,每次調用iterator的next()方法,會致使遊標向右移動,從而達到遍歷的目的。因此在單次循環中不能屢次調用next()方法,否則會致使每次循環時跳過一些元素,我在一些博客裏面看到了一些錯誤的寫法,好比這一篇《在ArrayList的循環中刪除元素,會不會出現問題?》文章中:

![image-20200101124822998](/Users/ruiwendaier/Library/Application Support/typora-user-images/image-20200101124822998.png)

先調用iterator.next()獲取元素,與elem進行比較,若是相等,再調用list.remove(iterator.next());來移除元素,這個時候的iterator.next()其實已經不是與elem相等的元素了,而是後一個元素了,咱們能夠寫個demo來測試一下

ArrayList<Integer> arrayList = new ArrayList();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
arrayList.add(6);
arrayList.add(7);

Integer elem = 3;
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
    System.out.println(arrayList);
    if(iterator.next().equals(elem)) {
    		arrayList.remove(iterator.next());
    }
} 
複製代碼

輸出結果以下:

[1, 2, 3, 4, 5, 6, 7]
[1, 2, 3, 4, 5, 6, 7]
[1, 2, 3, 4, 5, 6, 7]
[1, 2, 3, 5, 6, 7]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.test.ArrayListTest1.main(ArrayListTest1.java:29)
複製代碼

能夠看到移除的元素其實不是3,而是3以後的元素,由於調用了兩次next()方法,致使遊標多移動了。因此應該使用Integer value = iterator.next();將元素取出進行判斷。

第5種方法 - Iterator遍歷,使用Iterator的remove刪除元素(結果:正確刪除)

Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
    Integer value = iterator.next();
    if (value.equals(3)) {//3是須要刪除的元素
        iterator.remove();
    }
}
複製代碼

輸出結果:

當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 3, 4, 5]
當前arrayList是[1, 2, 3, 4, 5]
當前arrayList是[1, 2, 4, 5]
當前arrayList是[1, 2, 4, 5]
當前arrayList是[1, 2, 4, 5]
複製代碼

能夠正確刪除元素。

跟第3種和第4種方法的區別在因而使用iterator.remove();來移除元素,而在remove()方法中會對iterator的expectedModCount變量進行更新,因此在下次循環調用iterator.next()方法時,expectedModCount與modCount相等,不會拋出異常。

HashMap遍歷時刪除元素的幾種姿式

首先結論以下:

第1種方法 - for-each遍歷HashMap.entrySet,使用HashMap.remove()刪除(結果:拋出異常)。

第2種方法-for-each遍歷HashMap.keySet,使用HashMap.remove()刪除(結果:拋出異常)。

第3種方法-使用HashMap.entrySet().iterator()遍歷刪除(結果:正確刪除)。

下面讓咱們來詳細探究一下緣由吧!

HashMap的遍歷刪除方法與ArrayList的大同小異,只是api的調用方式不一樣。首先初始化一個HashMap,咱們要刪除key包含"3"字符串的鍵值對。

HashMap<String,Integer> hashMap = new HashMap<String,Integer>();
hashMap.put("key1",1);
hashMap.put("key2",2);
hashMap.put("key3",3);
hashMap.put("key4",4);
hashMap.put("key5",5);
hashMap.put("key6",6);
複製代碼

第1種方法 - for-each遍歷HashMap.entrySet,使用HashMap.remove()刪除(結果:拋出異常)

for (Map.Entry<String,Integer> entry: hashMap.entrySet()) {
        String key = entry.getKey();
        if(key.contains("3")){
            hashMap.remove(entry.getKey());
        }
     System.out.println("當前HashMap是"+hashMap+" 當前entry是"+entry);

}
複製代碼

輸出結果:

當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前entry是key1=1
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前entry是key2=2
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前entry是key5=5
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前entry是key6=6
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 當前entry是key3=3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1463)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1461)
	at com.test.HashMapTest.removeWayOne(HashMapTest.java:29)
	at com.test.HashMapTest.main(HashMapTest.java:22)
複製代碼

第2種方法-for-each遍歷HashMap.keySet,使用HashMap.remove()刪除(結果:拋出異常)

Set<String> keySet = hashMap.keySet();
for(String key : keySet){
    if(key.contains("3")){
        keySet.remove(key);
    }
    System.out.println("當前HashMap是"+hashMap+" 當前key是"+key);
}
複製代碼

輸出結果以下:

當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前key是key1
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前key是key2
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前key是key5
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前key是key6
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 當前key是key3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
	at java.util.HashMap$KeyIterator.next(HashMap.java:1453)
	at com.test.HashMapTest.removeWayTwo(HashMapTest.java:40)
	at com.test.HashMapTest.main(HashMapTest.java:23)
複製代碼

第3種方法-使用HashMap.entrySet().iterator()遍歷刪除(結果:正確刪除)

Iterator<Map.Entry<String, Integer>> iterator  = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    if(entry.getKey().contains("3")){
        iterator.remove();
    }
    System.out.println("當前HashMap是"+hashMap+" 當前entry是"+entry);
}
複製代碼

輸出結果:

當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key1=1
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key2=2
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key5=5
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key6=6
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key4=4
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 當前entry是deletekey=3
複製代碼

第1種方法和第2種方法拋出ConcurrentModificationException異常與上面ArrayList錯誤遍歷-刪除方法的緣由一致,HashIterator也有一個expectedModCount,在遍歷時獲取下一個元素時,會調用next()方法,而後對 expectedModCount和modCount進行判斷,不一致就拋出ConcurrentModificationException異常。

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    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);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    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;
    }

    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;
    }
}

複製代碼

PS:ConcurrentModificationException是什麼?

根據ConcurrentModificationException的文檔介紹,一些對象不容許併發修改,當這些修改行爲被檢測到時,就會拋出這個異常。(例如一些集合不容許一個線程一邊遍歷時,另外一個線程去修改這個集合)。

一些集合(例如Collection, Vector, ArrayList,LinkedList, HashSet, Hashtable, TreeMap, AbstractList, Serialized Form)的Iterator實現中,若是提供這種併發修改異常檢測,那麼這些Iterator能夠稱爲是"fail-fast Iterator",意思是快速失敗迭代器,就是檢測到併發修改時,直接拋出異常,而不是繼續執行,等到獲取到一些錯誤值時在拋出異常。

異常檢測主要是經過modCount和expectedModCount兩個變量來實現的,

  • modCount 集合被修改的次數,通常是被集合(ArrayList之類的)持有,每次調用add(),remove()方法會致使modCount+1

  • expectedModCount 期待的modCount,通常是被Iterator(ArrayList.iterator()方法返回的iterator對象)持有,通常在Iterator初始化時會賦初始值,在調用Iterator的remove()方法時會對expectedModCount進行更新。(能夠看看上面的ArrayList.Itr源碼)

而後在Iterator調用next()遍歷元素時,會調用checkForComodification()方法比較modCount和expectedModCount,不一致就拋出ConcurrentModificationException。

單線程操做Iterator不當時也會拋出ConcurrentModificationException異常。(上面的例子就是) WechatIMG4995.jpeg

總結

由於ArrayList和HashMap的Iterator都是上面所說的「fail-fast Iterator」,Iterator在獲取下一個元素,刪除元素時,都會比較expectedModCount和modCount,不一致就會拋出異常。

因此當使用Iterator遍歷元素(for-each遍歷底層實現也是Iterator)時,須要刪除元素,必定須要使用 Iterator的remove()方法 來刪除,而不是直接調用ArrayList或HashMap自身的remove()方法,不然會致使Iterator中的expectedModCount沒有及時更新,以後獲取下一個元素或者刪除元素時,expectedModCount和modCount不一致,而後拋出ConcurrentModificationException異常。

一塊兒學習交流

建了一個技術交流羣,歡迎你們掃碼加入!會分享一些我本身在看的一些技術資料給你們!

相關文章
相關標籤/搜索