編程坑太多,Map 集合怎么也有這么多坑?一不小心又踩了好幾個!


點贊再看,養成習慣,微信搜索『程序通事』,關注就完事了!
點擊查看更多歷史文章

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

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

本文設計知識點如下:

不是所有的 Map 都能包含 null

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

代碼示例如下:

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

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

美滋滋提交了代碼,然后當天上線的時候,就發現炸了。。。

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

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

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

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

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

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

來源: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 中存入 null,就可以從容避免上面問題。

自定義對象為 key

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

示例代碼如下:

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

翻看一下 HashMap#put 的源碼:

以下代碼基於 JDK1.7

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

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

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

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

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

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

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

當我們修改了金額字段后,導致 Goods 對象 hashcode 產生的了變化,從而導致 get 方法無法獲取到值。

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

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

錯用 ConcurrentHashMap 導致線程不安全

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

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

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

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

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

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

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

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

List 集合這些坑,Map 中也有

上一篇文章中我們提過,Arrays#asListList#subList 返回 List 將會與原集合互相影響,且可能並不支持 add 等方法。同樣的,這些坑爹的特性在 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 這三個方法產生的集合,建議再來個套娃。

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

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

總結

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

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

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

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

最后(關注,點贊,轉發三連)

你在使用 Map 的過程還踩過什么坑,歡迎留言討論。

我是樓下小黑哥,我們下篇文章再見~

記住我們的約定,微信搜索『程序通事』,快來關注哦!

歡迎關注我的公眾號:程序通事,獲得日常干貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM