1. Map 集合怎么也有這么多坑?一不小心又踩了好幾個!|原創(chuàng)

        共 3358字,需瀏覽 7分鐘

         ·

        2020-05-10 23:22


        39a8b3f3046b5afa7a46e27cf15451b9.webp

        上一篇 List 踩坑文章中,我們提到幾個比較容易踩坑的點。作為 List 集合好兄弟 Map,我們也是天天都在使用,一不小心也會踩坑。

        今天我就來總結(jié)這些常見的坑,再撈自己一手,防止后續(xù)同學(xué)再繼續(xù)踩坑。

        本文設(shè)計知識點如下:

        2cfb8d3af550aff85699dd18e5804c5c.webp

        不是所有的 Map 都能包含 ?null

        這個踩坑經(jīng)歷還是發(fā)生在實習(xí)的時候,那時候有這樣一段業(yè)務(wù)代碼,功能很簡單,從 XML 中讀取相關(guān)配置,存入 Map 中。

        代碼示例如下:

        e01ebf4b617d3b1acdbd57b9c2baa55b.webp

        那時候正好有個小需求,需要改動一下這段業(yè)務(wù)代碼。改動的過程中,突然想到 HashMap 并發(fā)過程可能導(dǎo)致死鎖的問題。

        于是改動了一下這段代碼,將 HashMap 修改成了 ConcurrentHashMap

        美滋滋提交了代碼,然后當(dāng)天上線的時候,就發(fā)現(xiàn)炸了。。。

        應(yīng)用啟動過程發(fā)生 NPE 問題,導(dǎo)致應(yīng)用啟動失敗。

        8eb29af0fcf0e4faa5d8e9434bfcf975.webp

        根據(jù)異常日志,很快就定位到了問題原因。由于 XML 某一項配置問題,導(dǎo)致讀取元素為 null,然后元素置入到 ConcurrentHashMap 中,拋出了空指針異常。

        這不科學(xué)啊!之前 HashMap 都沒問題,都可以存在 null,為什么它老弟 ConcurrentHashMap 就不可以?

        ac81a47213a145603906ffb2f9226c9e.webp

        翻閱了一下 ConcurrentHashMap#put 方法的源碼,開頭就看到了對 KV 的判空校驗。

        f11fb4d2d2dbf6c21d99767b72e0db65.webp

        看到這里,不知道你有沒有疑惑,為什么 ConcurrentHashMapHashMap 設(shè)計的判斷邏輯不一樣?

        求助了下萬能的 Google,找到 Doug Lea 老爺子的回答:

        629567cff6a22aa1189305b57acbd321.webp來源:http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002485.html

        總結(jié)一下:

        • null 會引起歧義,如果 value 為 null,我們無法得知是值為 null,還是 key 未映射具體值?
        • Doug Lea 并不喜歡 null,認(rèn)為 null 就是個隱藏的炸彈。

        上面提到 Josh Bloch 正是 HashMap 作者,他與 Doug Lea 在 null 問題意見并不一致。

        也許正是因為這些原因,從而導(dǎo)致 ConcurrentHashMapHashMap 對于 null 處理并不一樣。

        最后貼一下常用 Map 子類集合對于 null 存儲情況:

        9e892c3569c076ec4825a5d3e44094d8.webp

        上面的實現(xiàn)類約束,都太不一樣,有點不好記憶。其實只要我們在加入元素之前,主動去做空指針判斷,不要在 Map 中存入 null,就可以從容避免上面問題。

        自定義對象為 key

        先來看個簡單的例子,我們自定義一個 Goods 商品類,將其作為 Key 存在 Map 中。

        示例代碼如下:

        dcfcb627ad4f308897cf864313ea8051.webp

        上面代碼中,第二次我們加入一個相同的商品,原本我們期望新加入的值將會替換原來舊值。但是實際上這里并沒有替換成功,反而又加入一對鍵值。

        翻看一下 HashMap#put 的源碼:

        以下代碼基于 JDK1.7

        b8d9aad5ed37dc24fcf2dabb8285d28c.webp

        這里首先判斷 hashCode 計算產(chǎn)生的 hash,如果相等,再判斷 equals 的結(jié)果。但是由于 Goods對象未重寫的hashCodeequals 方法,默認(rèn)情況下 hashCode 將會使用父類對象 Object 方法邏輯。

        Object#hashCode 是一個 native 方法,默認(rèn)將會為每一個對象生成不同 hashcode與內(nèi)存地址有關(guān)),這就導(dǎo)致上面的情況。

        所以如果需要使用自定義對象做為 Map 集合的 key,那么一定記得重寫hashCodeequals 方法。

        然后當(dāng)你為自定義對象重寫上面兩個方法,接下去又可能踩坑另外一個坑。

        1e5092dc9d86df9eb3f3359a1b0e4e8d.webp

        使用 lombok 的 EqualsAndHashCode 自動重寫 hashCodeequals 方法。

        上面的代碼中,當(dāng) Map 中置入自定義對象后,接著修改了商品金額。然后當(dāng)我們想根據(jù)同一個對象取出 Map 中存的值時,卻發(fā)現(xiàn)取不出來了。

        上面的問題主要是因為 get 方法是根據(jù)對象 的 hashcode 計算產(chǎn)生的 hash 值取定位內(nèi)部存儲位置。

        c60a81dd870417e09ca630d646a87b3b.webp

        當(dāng)我們修改了金額字段后,導(dǎo)致 Goods 對象 hashcode 產(chǎn)生的了變化,從而導(dǎo)致 get 方法無法獲取到值。

        通過上面兩種情況,可以看到使用自定義對象作為 Map 集合 key,還是挺容易踩坑的。

        所以盡量避免使用自定義對象作為 Map 集合 key,如果一定要使用,記得重寫 hashCodeequals 方法。另外還要保證這是一個不可變對象,即對象創(chuàng)建之后,無法再修改里面字段值。

        錯用 ConcurrentHashMap 導(dǎo)致線程不安全

        之前的文章『每天都在用 Map,這些核心技術(shù)你知道嗎?』我們說過 HashMap 是一個線程不安全的容器,多線程環(huán)境為了線程安全,我們需要使用 ConcurrentHashMap代替。

        但是不要認(rèn)為使用了 ConcurrentHashMap 一定就能保證線程安全,在某些錯誤的使用場景下,依然會造成線程不安全。

        2975888b60da1071f634e63ecc5a8aad.webp

        上面示例代碼,我們原本期望輸出 1001,但是運行幾次,得到結(jié)果都是小于 1001。

        深入分析這個問題原因,實際上是因為第一步與第二步是一個組合邏輯,不是一個原子操作。

        ConcurrentHashMap 只能保證這兩步單的操作是個原子操作,線程安全。但是并不能保證兩個組合邏輯線程安全,很有可能 A 線程剛通過 get 方法取到值,還未來得及加 1,線程發(fā)生了切換,B 線程也進(jìn)來取到同樣的值。

        這個問題同樣也發(fā)生在其他線程安全的容器,比如 Vector等。

        上面的問題解決辦法也很簡單,加鎖就可以解決,不過這樣就會使性能大打折扣,所以不太推薦。

        我們可以使用 AtomicInteger 解決以上的問題。

        71fae59bdd465e6af6ae4a7288fb26a5.webp

        List 集合這些坑,Map 中也有

        上一篇文章中我們提過,Arrays#asListList#subList 返回 List 將會與原集合互相影響,且可能并不支持 add 等方法。同樣的,這些坑爹的特性在 Map 中也存在,一不小心,將會再次掉坑。

        72c47c373c358fd45586b6c313b92792.webp

        Map 接口除了支持增刪改查功能以外,還有三個特有的方法,能返回所有 key,返回所有的 value,返回所有 kv 鍵值對。

        //?返回?key?的?set?視圖
        Set?keySet();
        //?返回所有?value???Collection?視圖
        Collection?values()
        ;
        //?返回?key-value?的?set?視圖
        Set>?entrySet();

        這三個方法創(chuàng)建返回新集合,底層其實都依賴的原有 Map 中數(shù)據(jù),所以一旦 Map 中元素變動,就會同步影響返回的集合。

        另外這三個方法返回新集合,是不支持的新增以及修改操作的,但是卻支持 clear、remove 等操作。

        示例代碼如下:

        0772a2ae00f5f59372a007cb477735e8.webp

        所以如果需要對外返回 Map 這三個方法產(chǎn)生的集合,建議再來個套娃。

        new?ArrayList<>(map.values());

        最后再簡單提一下,使用 foreach 方式遍歷新增/刪除 Map 中元素,也將會和 List 集合一樣,拋出 ConcurrentModificationException。

        總結(jié)

        從上面文章可以看到不管是 List 提供的方法返回集合,還是 Map 中方法返回集合,底層實際還是使用原有集合的元素,這就導(dǎo)致兩者將會被互相影響。所以如果需要對外返回,請使用套娃大法,這樣讓別人用的也安心。

        第二, Map 各個實現(xiàn)類對于 null 的約束都不太一樣,這里建議在 Map 中加入元素之前,主動進(jìn)行空指針判斷,提前發(fā)現(xiàn)問題。

        第三,慎用自定義對象作為 Map 中的 key,如果需要使用,一定要重寫 hashCodeequals 方法,并且還要保證這是個不可變對象。

        第三,ConcurrentHashMap 是線程安全的容器,但是不要思維定勢,不要片面認(rèn)為使用 ConcurrentHashMap 就會線程安全。


        文末福利:

        我給大家準(zhǔn)備了一份超級優(yōu)質(zhì)的視頻資料,通往BAT的必經(jīng)之路,看完可以直接去面試了!截圖如下:


        9962fe8102077354218a017dec42782c.webp

        領(lǐng)取方式如下:

        1. 掃描關(guān)注下方:Java開發(fā)寶典

        2. 在后臺回復(fù)“BAT”,即可領(lǐng)取



        點贊是最大的支持?5e114a4d0734eb3dca5d8d1931e06f1a.webp

        瀏覽 96
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 亚洲一区二区三区桃乃木香奈 | 操大奶美女 | 日本丶国产丶欧美色综合 | 和女同学裸睡后我进去了 | 国产偷人爽久久久久久 |