<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          HashMap 為什么不能一邊遍歷一遍刪除

          共 8124字,需瀏覽 17分鐘

           ·

          2023-02-03 23:16

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          (給ImportNew加星標(biāo),提高Java技能)

          前段時(shí)間,同事在代碼中 KW 掃描的時(shí)候出現(xiàn)這樣一條:


          上面出現(xiàn)這樣的原因是在使用 foreach 對(duì) HashMap 進(jìn)行遍歷時(shí),同時(shí)進(jìn)行 put 賦值操作會(huì)有問題,異常 ConcurrentModificationException。

          于是幫同簡(jiǎn)單的看了一下,印象中集合類在進(jìn)行遍歷時(shí)同時(shí)進(jìn)行刪除或者添加操作時(shí)需要謹(jǐn)慎,一般使用迭代器進(jìn)行操作。

          于是告訴同事,應(yīng)該使用迭代器 Iterator 來(lái)對(duì)集合元素進(jìn)行操作。同事問我為什么?這一下子把我問蒙了?對(duì)啊,只是記得這樣用不可以,但是好像自己從來(lái)沒有細(xì)究過為什么?

          于是今天決定把這個(gè) HashMap 遍歷操作好好地研究一番,防止采坑!

          foreach 循環(huán)?


          Java foreach 語(yǔ)法是在 JDK 1.5 時(shí)加入的新特性,主要是當(dāng)作 for 語(yǔ)法的一個(gè)增強(qiáng),那么它的底層到底是怎么實(shí)現(xiàn)的呢?下面我們來(lái)好好研究一下:

          foreach 語(yǔ)法內(nèi)部,對(duì) collection 是用 iterator 迭代器來(lái)實(shí)現(xiàn)的,對(duì)數(shù)組是用下標(biāo)遍歷來(lái)實(shí)現(xiàn)。Java 5 及以上的編譯器隱藏了基于 iteration 和數(shù)組下標(biāo)遍歷的內(nèi)部實(shí)現(xiàn)。

          注意:這里說的是“Java 編譯器”或 Java 語(yǔ)言對(duì)其實(shí)現(xiàn)做了隱藏,而不是某段 Java 代碼對(duì)其實(shí)現(xiàn)做了隱藏,也就是說,我們?cè)谌魏我欢?JDK 的 Java 代碼中都找不到這里被隱藏的實(shí)現(xiàn)。這里的實(shí)現(xiàn),隱藏在了Java 編譯器中,查看一段 foreach 的 Java 代碼編譯成的字節(jié)碼,從中揣測(cè)它到底是怎么實(shí)現(xiàn)的了。

          我們寫一個(gè)例子來(lái)研究一下:

          public class HashMapIteratorDemo {    String[] arr = {        "aa",        "bb",        "cc"    };
          public void test1() { for (String str: arr) {} }}

          將上面的例子轉(zhuǎn)為字節(jié)碼反編譯一下(主函數(shù)部分):


          也許我們不能很清楚這些指令到底有什么作用,但是我們可以對(duì)比一下下面段代碼產(chǎn)生的字節(jié)碼指令:

          public class HashMapIteratorDemo2 {    String[] arr = {        "aa",        "bb",        "cc"    };
          public void test1() { for (int i = 0; i < arr.length; i++) { String str = arr[i]; } }}


          看看兩個(gè)字節(jié)碼文件,有木有發(fā)現(xiàn)指令幾乎相同,如果還有疑問我們?cè)倏纯磳?duì)集合的 foreach 操作:

          通過 foreach 遍歷集合:

          public class HashMapIteratorDemo3 {    List < Integer > list = new ArrayList < > ();
          public void test1() { list.add(1); list.add(2); list.add(3);
          for (Integer var: list) {} }}

          通過 Iterator 遍歷集合:

          public class HashMapIteratorDemo4 {    List < Integer > list = new ArrayList < > ();
          public void test1() { list.add(1); list.add(2); list.add(3);
          Iterator < Integer > it = list.iterator(); while (it.hasNext()) { Integer var = it.next(); } }}

          將兩個(gè)方法的字節(jié)碼對(duì)比如下:


          我們發(fā)現(xiàn)兩個(gè)方法字節(jié)碼指令操作幾乎一模一樣;

          這樣我們可以得出以下結(jié)論:

          對(duì)集合來(lái)說,由于集合都實(shí)現(xiàn)了 Iterator 迭代器,foreach 語(yǔ)法最終被編譯器轉(zhuǎn)為了對(duì) Iterator.next() 的調(diào)用;

          對(duì)于數(shù)組來(lái)說,就是轉(zhuǎn)化為對(duì)數(shù)組中的每一個(gè)元素的循環(huán)引用。

          HashMap 遍歷集合并對(duì)集合元素進(jìn)行 remove、put、add


          1、現(xiàn)象


          根據(jù)以上分析,我們知道 HashMap 底層是實(shí)現(xiàn)了 Iterator 迭代器的 ,那么理論上我們也是可以使用迭代器進(jìn)行遍歷的,這倒是不假,例如下面:

          public class HashMapIteratorDemo5 {    public static void main(String[] args) {        Map < Integer, String > map = new HashMap < > ();        map.put(1, "aa");        map.put(2, "bb");        map.put(3, "cc");
          for (Map.Entry < Integer, String > entry: map.entrySet()) { int k = entry.getKey(); String v = entry.getValue(); System.out.println(k + " = " + v); } }}

          輸出:


          OK,遍歷沒有問題,那么操作集合元素 remove、put、add 呢?

          public class HashMapIteratorDemo5 {    public static void main(String[] args) {        Map < Integer, String > map = new HashMap < > ();        map.put(1, "aa");        map.put(2, "bb");        map.put(3, "cc");
          for (Map.Entry < Integer, String > entry: map.entrySet()) { int k = entry.getKey(); if (k == 1) { map.put(1, "AA"); } String v = entry.getValue(); System.out.println(k + " = " + v); } }}

          執(zhí)行結(jié)果:


          執(zhí)行沒有問題,put 操作也成功了。

          但是!但是!但是!問題來(lái)了?。?!

          我們知道 HashMap 是一個(gè)線程不安全的集合類,如果使用 foreach 遍歷時(shí),進(jìn)行add, remove 操作會(huì) java.util.ConcurrentModificationException 異常。put 操作可能會(huì)拋出該異常。(為什么說可能,這個(gè)我們后面解釋)

          為什么會(huì)拋出這個(gè)異常呢?

          我們先去看一下 Java API 文檔對(duì) HasMap 操作的解釋吧。


          翻譯過來(lái)大致的意思就是:該方法是返回此映射中包含的鍵的集合視圖。

          集合由映射支持,如果在對(duì)集合進(jìn)行迭代時(shí)修改了映射(通過迭代器自己的移除操作除外),則迭代的結(jié)果是未定義的。集合支持元素移除,通過 Iterator.remove、set.remove、removeAll、retainal 和 clear 操作從映射中移除相應(yīng)的映射。簡(jiǎn)單說,就是通過 map.entrySet() 這種方式遍歷集合時(shí),不能對(duì)集合本身進(jìn)行 remove、add 等操作,需要使用迭代器進(jìn)行操作。

          對(duì)于 put 操作,如果這個(gè)操作時(shí)替換操作如上例中將第一個(gè)元素進(jìn)行修改,就沒有拋出異常,但是如果是使用 put 添加元素的操作,則肯定會(huì)拋出異常了。我們把上面的例子修改一下:

          public class HashMapIteratorDemo5 {    public static void main(String[] args) {        Map < Integer, String > map = new HashMap < > ();        map.put(1, "aa");        map.put(2, "bb");        map.put(3, "cc");
          for (Map.Entry < Integer, String > entry: map.entrySet()) { int k = entry.getKey(); if (k == 1) { map.put(4, "AA"); } String v = entry.getValue(); System.out.println(k + " = " + v);        } }}

          執(zhí)行出現(xiàn)異常:


          這就是驗(yàn)證了上面說的 put 操作可能會(huì)拋出 java.util.ConcurrentModificationException 異常。


          但是有疑問了,我們上面說過 foreach 循環(huán)就是通過迭代器進(jìn)行的遍歷啊?為什么到這里是不可以了呢?


          這里其實(shí)很簡(jiǎn)單,原因是我們的遍歷操作底層確實(shí)是通過迭代器進(jìn)行的,但是我們的 remove 等操作是通過直接操作 map 進(jìn)行的,如上例子:map.put(4, "AA"); //這里實(shí)際還是直接對(duì)集合進(jìn)行的操作,而不是通過迭代器進(jìn)行操作。所以依然會(huì)存在 ConcurrentModificationException 異常問題。


          2、細(xì)究底層原理


          我們?cè)偃タ纯?HashMap 的源碼,通過源代碼,我們發(fā)現(xiàn)集合在使用 Iterator 進(jìn)行遍歷時(shí)都會(huì)用到這個(gè)方法:

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

          這里 modCount 是表示 map 中的元素被修改了幾次(在移除,新加元素時(shí)此值都會(huì)自增),而 expectedModCount 是表示期望的修改次數(shù),在迭代器構(gòu)造的時(shí)候這兩個(gè)值是相等,如果在遍歷過程中這兩個(gè)值出現(xiàn)了不同步就會(huì)拋出 ConcurrentModificationException 異常。

          現(xiàn)在我們來(lái)看看集合 remove 操作:

          (1)HashMap 本身的 remove 實(shí)現(xiàn):


          public V remove(Object key) {    Node < K, V > e;    return (e = removeNode(hash(key), key, null, false, true)) == null ?        null : e.value;}

          (2)HashMap.KeySet 的 remove 實(shí)現(xiàn)

          public final boolean remove(Object key) {    return removeNode(hash(key), key, null, false, true) != null;}

          (3)HashMap.EntrySet 的 remove 實(shí)現(xiàn)

          public final boolean remove(Object o) {    if (o instanceof Map.Entry) {        Map.Entry << ? , ? > e = (Map.Entry << ? , ? > ) o;        Object key = e.getKey();        Object value = e.getValue();        return removeNode(hash(key), key, value, true, true) != null;    }    return false;}

          (4)HashMap.HashIterator 的 remove 方法實(shí)現(xiàn)

          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; //--這里將expectedModCount 與modCount進(jìn)行同步}

          以上四種方式都通過調(diào)用 HashMap.removeNode 方法來(lái)實(shí)現(xiàn)刪除key的操作。在 removeNode 方法內(nèi)只要移除了 key, modCount 就會(huì)執(zhí)行一次自增操作,此時(shí) modCount 就與 expectedModCount 不一致了;

          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 &&        ...        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; //----這里對(duì)modCount進(jìn)行了自增,可能會(huì)導(dǎo)致后面與expectedModCount不一致            --size;            afterNodeRemoval(node);            return node;        }    }    return null;}

          上面三種 remove 實(shí)現(xiàn)中,只有第三種 iterator 的 remove 方法在調(diào)用完 removeNode 方法后同步了 expectedModCount 值與 modCount 相同,所以在遍歷下個(gè)元素調(diào)用 nextNode 方法時(shí),iterator 方式不會(huì)拋異常。

          到這里是不是有一種恍然大明白的感覺呢!

          所以,如果需要對(duì)集合遍歷時(shí)進(jìn)行元素操作需要借助 Iterator 迭代器進(jìn)行,如下:

          public class HashMapIteratorDemo5 {    public static void main(String[] args) {        Map < Integer, String > map = new HashMap < > ();        map.put(1, "aa");        map.put(2, "bb");        map.put(3"cc");
          Iterator < Map.Entry < Integer, String >> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry < Integer, String > entry = it.next(); int key = entry.getKey(); if (key == 1) { it.remove(); } } }}
          轉(zhuǎn)自:你呀不牛,
          鏈接:juejin.cn/post/7114669787870920734

            

          1、社區(qū)糾紛不斷:程序員何苦為難程序員?

          2、該死的單元測(cè)試,寫起來(lái)到底有多痛?

          3、互聯(lián)網(wǎng)人為什么學(xué)不會(huì)擺爛

          4、為什么國(guó)外JetBrains做 IDE 就可以養(yǎng)活自己,國(guó)內(nèi)不行?區(qū)別在哪?

          5、相比高人氣的Rust、Go,為何 Java、C 在工具層面進(jìn)展緩慢?

          6、讓程序員早點(diǎn)下班的《技術(shù)寫作指南》

          點(diǎn)

          點(diǎn)

          點(diǎn)點(diǎn)

          點(diǎn)在看


          瀏覽 53
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美中文字幕在线视频观看 | 色婷婷综合激情网 | 国产黄片在线视频 | 人人人人操 | 大香蕉亚洲成人 |