天天看點

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

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

今天我就來總結這些常見的坑,再撈自己一手,防止後續同學再繼續踩坑。

本文設計知識點如下:

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

這個踩坑經曆還是發生在實習的時候,那時候有這樣一段業務代碼,功能很簡單,從 XML 中讀取相關配置,存入 Map 中。

代碼示例如下:

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

那時候正好有個小需求,需要改動一下這段業務代碼。改動的過程中,突然想到 <code>HashMap</code> 并發過程可能導緻死鎖的問題。

于是改動了一下這段代碼,将 <code>HashMap</code> 修改成了<code>ConcurrentHashMap</code>。

美滋滋送出了代碼,然後當天上線的時候,就發現炸了。。。

應用啟動過程發生 NPE 問題,導緻應用啟動失敗。

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

根據異常日志,很快就定位到了問題原因。由于 XML 某一項配置問題,導緻讀取元素為 null,然後元素置入到 <code>ConcurrentHashMap</code> 中,抛出了空指針異常。

這不科學啊!之前 <code>HashMap</code> 都沒問題,都可以存在 null,為什麼它老弟 <code>ConcurrentHashMap</code> 就不可以?

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

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

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

看到這裡,不知道你有沒有疑惑,為什麼 <code>ConcurrentHashMap</code> 與 <code>HashMap</code> 設計的判斷邏輯不一樣?

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

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

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

總結一下:

null 會引起歧義,如果 value 為 null,我們無法得知是值為 null,還是 key 未映射具體值?

Doug Lea 并不喜歡 null,認為 null 就是個隐藏的炸彈。

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

也許正是因為這些原因,進而導緻 <code>ConcurrentHashMap</code> 與 <code>HashMap</code> 對于 null 處理并不一樣。

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

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

自定義對象為 key上面的實作類限制,都太不一樣,有點不好記憶。其實隻要我們在加入元素之前,主動去做空指針判斷,不要在 Map 中存入 null,就可以從容避免上面問題。

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

示例代碼如下:

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

翻看一下 <code>HashMap#put</code> 的源碼:上面代碼中,第二次我們加入一個相同的商品,原本我們期望新加入的值将會替換原來舊值。但是實際上這裡并沒有替換成功,反而又加入一對鍵值。

以下代碼基于 JDK1.7
Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

這裡首先判斷 <code>hashCode</code> 計算産生的 hash,如果相等,再判斷 <code>equals</code>的結果。但是由于 <code>Goods</code>對象未重寫的<code>hashCode</code> 與 <code>equals</code> 方法,預設情況下 <code>hashCode</code> 将會使用父類對象 Object 方法邏輯。

而 <code>Object#hashCode</code> 是一個 native 方法,預設将會為每一個對象生成不同 hashcode(與記憶體位址有關),這就導緻上面的情況。

是以如果需要使用自定義對象做為 Map 集合的 key,那麼一定記得重寫<code>hashCode</code> 與 <code>equals</code> 方法。

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

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!
使用 lombok 的 <code>EqualsAndHashCode</code> 自動重寫 <code>hashCode</code> 與 <code>equals</code> 方法。

上面的代碼中,當 Map 中置入自定義對象後,接着修改了商品金額。然後當我們想根據同一個對象取出 Map 中存的值時,卻發現取不出來了。

上面的問題主要是因為 <code>get</code> 方法是根據對象 的 hashcode 計算産生的 hash 值取定位内部存儲位置。

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

當我們修改了金額字段後,導緻 <code>Goods</code> 對象 hashcode 産生的了變化,進而導緻 get 方法無法擷取到值。

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

是以盡量避免使用自定義對象作為 Map 集合 key,如果一定要使用,記得重寫 <code>hashCode</code> 與 <code>equals</code> 方法。另外還要保證這是一個不可變對象,即對象建立之後,無法再修改裡面字段值。

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

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

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

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

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

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

這個問題同樣也發生在其他線程安全的容器,比如 <code>Vector</code>等。

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

我們可以使用 <code>AtomicInteger</code> 解決以上的問題。

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

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

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

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

這三個方法建立傳回新集合,底層其實都依賴的原有 Map 中資料,是以一旦 Map 中元素變動,就會同步影響傳回的集合。

另外這三個方法傳回新集合,是不支援的新增以及修改操作的,但是卻支援 <code>clear、remove</code> 等操作。

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

是以如果需要對外傳回 Map 這三個方法産生的集合,建議再來個套娃。

最後再簡單提一下,使用 <code>foreach</code> 方式周遊新增/删除 Map 中元素,也将會和 List 集合一樣,抛出 <code>ConcurrentModificationException</code>。

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

第二, Map 各個實作類對于 null 的限制都不太一樣,這裡建議在 Map 中加入元素之前,主動進行空指針判斷,提前發現問題。

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

第三,<code>ConcurrentHashMap</code> 是線程安全的容器,但是不要思維定勢,不要片面認為使用 <code>ConcurrentHashMap</code> 就會線程安全。

Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!

https://mp.weixin.qq.com/s/2uuP_UiuGhcYDOi768V_zw