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

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

上一篇 List 踩坑文章中,咱們提到幾個比較容易踩坑的點。做爲 List 集合好兄弟 Map,咱們也是每天都在使用,一不當心也會踩坑。html

今天我就來總結這些常見的坑,再撈本身一手,防止後續同窗再繼續踩坑。安全

本文設計知識點以下:多線程

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

不是全部的 Map 都能包含 null

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

代碼示例以下:ide

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

那時候正好有個小需求,須要改動一下這段業務代碼。改動的過程當中,忽然想到 HashMap 併發過程可能致使死鎖的問題。性能

因而改動了一下這段代碼,將 HashMap 修改爲了 ConcurrentHashMap線程

美滋滋提交了代碼,而後當天上線的時候,就發現炸了。。。設計

應用啓動過程發生 NPE 問題,致使應用啓動失敗。3d

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

根據異常日誌,很快就定位到了問題緣由。因爲 XML 某一項配置問題,致使讀取元素爲 null,而後元素置入到 ConcurrentHashMap 中,拋出了空指針異常。指針

這不科學啊!以前 HashMap 都沒問題,均可以存在 null,爲何它老弟 ConcurrentHashMap 就不能夠?

Map 集合怎麼也有這麼多坑?一不當心又踩了好幾個!
翻閱了一下 ConcurrentHashMap#put 方法的源碼,開頭就看到了對 KV 的判空校驗。

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

看到這裏,不知道你有沒有疑惑,爲何 ConcurrentHashMapHashMap 設計的判斷邏輯不同?

求助了下萬能的 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 正是 HashMap 做者,他與 Doug Lea 在 null 問題意見並不一致。

也許正是由於這些緣由,從而致使 ConcurrentHashMapHashMap 對於 null 處理並不同。

最後貼一下經常使用 Map 子類集合對於 null 存儲狀況:

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

上面的實現類約束,都太不同,有點很差記憶。其實只要咱們在加入元素以前,主動去作空指針判斷,不要在 Map 中存入 null,就能夠從容避免上面問題。

自定義對象爲 key

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

示例代碼以下:

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

上面代碼中,第二次咱們加入一個相同的商品,本來咱們指望新加入的值將會替換原來舊值。可是實際上這裏並無替換成功,反而又加入一對鍵值。

翻看一下 HashMap#put 的源碼:

如下代碼基於 JDK1.7

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

這裏首先判斷 hashCode 計算產生的 hash,若是相等,再判斷 equals 的結果。可是因爲 Goods對象未重寫的hashCodeequals 方法,默認狀況下 hashCode 將會使用父類對象 Object 方法邏輯。

Object#hashCode 是一個 native 方法,默認將會爲每個對象生成不一樣 hashcode與內存地址有關),這就致使上面的狀況。

因此若是須要使用自定義對象作爲 Map 集合的 key,那麼必定記得重寫hashCodeequals 方法。

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

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

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

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

上面的問題主要是由於 get 方法是根據對象 的 hashcode 計算產生的 hash 值取定位內部存儲位置。

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

當咱們修改了金額字段後,致使 Goods 對象 hashcode 產生的了變化,從而致使 get 方法沒法獲取到值。

經過上面兩種狀況,能夠看到使用自定義對象做爲 Map 集合 key,仍是挺容易踩坑的。

因此儘可能避免使用自定義對象做爲 Map 集合 key,若是必定要使用,記得重寫 hashCodeequals 方法。另外還要保證這是一個不可變對象,即對象建立以後,沒法再修改裏面字段值。

錯用 ConcurrentHashMap 致使線程不安全

以前的文章『天天都在用 Map,這些核心技術你知道嗎?』咱們說過 HashMap 是一個線程不安全的容器,多線程環境爲了線程安全,咱們須要使用 ConcurrentHashMap代替。

可是不要認爲使用了 ConcurrentHashMap 必定就能保證線程安全,在某些錯誤的使用場景下,依然會形成線程不安全。

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

上面示例代碼,咱們本來指望輸出 1001,可是運行幾回,獲得結果都是小於 1001

深刻分析這個問題緣由,其實是由於第一步與第二步是一個組合邏輯,不是一個原子操做。

ConcurrentHashMap 只能保證這兩步單的操做是個原子操做,線程安全。可是並不能保證兩個組合邏輯線程安全,頗有可能 A 線程剛經過 get 方法取到值,還將來得及加 1,線程發生了切換,B 線程也進來取到一樣的值。

這個問題一樣也發生在其餘線程安全的容器,好比 Vector等。

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

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

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

List 集合這些坑,Map 中也有

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

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

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

// 返回 key 的 set 視圖
Set<K> keySet();
// 返回全部 value   Collection 視圖
Collection<V> values();
// 返回 key-value 的 set 視圖
Set<Map.Entry<K, V>> entrySet();

這三個方法建立返回新集合,底層其實都依賴的原有 Map 中數據,因此一旦 Map 中元素變更,就會同步影響返回的集合。

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

示例代碼以下:

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

因此若是須要對外返回 Map 這三個方法產生的集合,建議再來個套娃。

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

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

總結

從上面文章能夠看到不論是 List 提供的方法返回集合,仍是 Map 中方法返回集合,底層實際仍是使用原有集合的元素,這就致使二者將會被互相影響。因此若是須要對外返回,請使用套娃大法,這樣讓別人用的也安心。

第二, Map 各個實現類對於 null 的約束都不太同樣,這裏建議在 Map 中加入元素以前,主動進行空指針判斷,提早發現問題。

第三,慎用自定義對象做爲 Map 中的 key,若是須要使用,必定要重寫 hashCodeequals 方法,而且還要保證這是個不可變對象。

第三,ConcurrentHashMap 是線程安全的容器,可是不要思惟定勢,不要片面認爲使用 ConcurrentHashMap 就會線程安全。

相關文章
相關標籤/搜索