編程坑太多,Map 集合怎麼也有這麼多坑?一不小心又踩了好幾個!
點贊再看,養成習慣,微信搜索『程式通事』,關注就完事了!
點擊查看更多歷史文章
上一篇 List 踩坑文章中,我們提到幾個比較容易踩坑的點。作為 List 集合好兄弟 Map,我們也是天天都在使用,一不小心也會踩坑。
今天我就來總結這些常見的坑,再撈自己一手,防止後續同學再繼續踩坑。
本文設計知識點如下:
不是所有的 Map 都能包含 null
這個踩坑經歷還是發生在實習的時候,那時候有這樣一段業務程式碼,功能很簡單,從 XML 中讀取相關配置,存入 Map 中。
程式碼示例如下:
那時候正好有個小需求,需要改動一下這段業務程式碼。改動的過程中,突然想到 HashMap
並發過程可能導致死鎖的問題。
於是改動了一下這段程式碼,將 HashMap
修改成了 ConcurrentHashMap
。
美滋滋提交了程式碼,然後當天上線的時候,就發現炸了。。。
應用啟動過程發生 NPE 問題,導致應用啟動失敗。
根據異常日誌,很快就定位到了問題原因。由於 XML 某一項配置問題,導致讀取元素為 null,然後元素置入到 ConcurrentHashMap
中,拋出了空指針異常。
這不科學啊! 之前 HashMap
都沒問題,都可以存在 null,為什麼它老弟 ConcurrentHashMap
就不可以?
翻閱了一下 ConcurrentHashMap#put
方法的源碼,開頭就看到了對 KV 的判空校驗。
看到這裡,不知道你有沒有疑惑,為什麼 ConcurrentHashMap
與 HashMap
設計的判斷邏輯不一樣?
求助了下萬能的 Google,找到 Doug Lea 老爺子的回答:
總結一下:
- null 會引起歧義,如果 value 為 null,我們無法得知是值為 null,還是 key 未映射具體值?
- Doug Lea 並不喜歡 null,認為 null 就是個隱藏的炸彈。
上面提到 Josh Bloch 正是 HashMap
作者,他與 Doug Lea 在 null 問題意見並不一致。
也許正是因為這些原因,從而導致 ConcurrentHashMap
與 HashMap
對於 null 處理並不一樣。
最後貼一下常用 Map 子類集合對於 null 存儲情況:
上面的實現類約束,都太不一樣,有點不好記憶。其實只要我們在加入元素之前,主動去做空指針判斷,不要在 Map 中存入 null,就可以從容避免上面問題。
自定義對象為 key
先來看個簡單的例子,我們自定義一個 Goods
商品類,將其作為 Key 存在 Map 中。
示例程式碼如下:
上面程式碼中,第二次我們加入一個相同的商品,原本我們期望新加入的值將會替換原來舊值。但是實際上這裡並沒有替換成功,反而又加入一對鍵值。
翻看一下 HashMap#put
的源碼:
以下程式碼基於 JDK1.7
這裡首先判斷 hashCode
計算產生的 hash,如果相等,再判斷 equals
的結果。但是由於 Goods
對象未重寫的hashCode
與 equals
方法,默認情況下 hashCode
將會使用父類對象 Object 方法邏輯。
而 Object#hashCode
是一個 native 方法,默認將會為每一個對象生成不同 hashcode(與記憶體地址有關),這就導致上面的情況。
所以如果需要使用自定義對象做為 Map 集合的 key,那麼一定記得重寫hashCode
與 equals
方法。
然後當你為自定義對象重寫上面兩個方法,接下去又可能踩坑另外一個坑。
使用 lombok 的
EqualsAndHashCode
自動重寫hashCode
與equals
方法。
上面的程式碼中,當 Map 中置入自定義對象後,接著修改了商品金額。然後當我們想根據同一個對象取出 Map 中存的值時,卻發現取不出來了。
上面的問題主要是因為 get
方法是根據對象 的 hashcode 計算產生的 hash 值取定位內部存儲位置。
當我們修改了金額欄位後,導致 Goods
對象 hashcode 產生的了變化,從而導致 get 方法無法獲取到值。
通過上面兩種情況,可以看到使用自定義對象作為 Map 集合 key,還是挺容易踩坑的。
所以盡量避免使用自定義對象作為 Map 集合 key,如果一定要使用,記得重寫 hashCode
與 equals
方法。另外還要保證這是一個不可變對象,即對象創建之後,無法再修改裡面欄位值。
錯用 ConcurrentHashMap 導致執行緒不安全
之前的文章『每天都在用 Map,這些核心技術你知道嗎?』我們說過 HashMap
是一個執行緒不安全的容器,多執行緒環境為了執行緒安全,我們需要使用 ConcurrentHashMap
代替。
但是不要認為使用了 ConcurrentHashMap
一定就能保證執行緒安全,在某些錯誤的使用場景下,依然會造成執行緒不安全。
上面示例程式碼,我們原本期望輸出 1001,但是運行幾次,得到結果都是小於 1001。
深入分析這個問題原因,實際上是因為第一步與第二步是一個組合邏輯,不是一個原子操作。
ConcurrentHashMap
只能保證這兩步單的操作是個原子操作,執行緒安全。但是並不能保證兩個組合邏輯執行緒安全,很有可能 A 執行緒剛通過 get 方法取到值,還未來得及加 1,執行緒發生了切換,B 執行緒也進來取到同樣的值。
這個問題同樣也發生在其他執行緒安全的容器,比如 Vector
等。
上面的問題解決辦法也很簡單,加鎖就可以解決,不過這樣就會使性能大打折扣,所以不太推薦。
我們可以使用 AtomicInteger
解決以上的問題。
List 集合這些坑,Map 中也有
上一篇文章中我們提過,Arrays#asList
與 List#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,如果需要使用,一定要重寫 hashCode
與 equals
方法,並且還要保證這是個不可變對象。
第三,ConcurrentHashMap
是執行緒安全的容器,但是不要思維定勢,不要片面認為使用 ConcurrentHashMap
就會執行緒安全。
最後(關注,點贊,轉發三連)
你在使用 Map 的過程還踩過什麼坑,歡迎留言討論。
我是樓下小黑哥,我們下篇文章再見~
記住我們的約定,微信搜索『程式通事』,快來關注哦!
歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn