網(wǎng)站一般在哪建設(shè)網(wǎng)絡(luò)推廣人員
一、背景
最近設(shè)計某個類庫時使用了 ConcurrentHashMap
最后遇到了 value 為 null 時報了空指針異常的坑。
本文想探討下以下幾個問題:
(1) Map
接口的常見子類的 kv 對 null 的支持情況。
(2)為什么 ConcurrentHashMap
不支持 key 和 value 為 null?
(3)如果 value 可能為 null ,該如何處理?
(4)有哪些線程安全的 Java Map 類?
(5) 常見的 Map
接口的子類,如 HashMap
、TreeMap
、ConcurrentHashMap
、ConcurrentSkipListMap
的使用場景。
二、探究
2.1 Map
接口的常見子類的 kv 對 null 的支持情況
下圖來源于孤盡老師 《碼出高效》 第 6 章 數(shù)據(jù)結(jié)構(gòu)與集合
2.2 為什么 ConcurrentHashMap
不支持 key 和 value 為 null?
從 java.util.concurrent.ConcurrentHashMap#put
方法的注釋和源碼中可以非常容易得看出,不支持 key 和 value null。
/*** Maps the specified key to the specified value in this table.* Neither the key nor the value can be null.** <p>The value can be retrieved by calling the {@code get} method* with a key that is equal to the original key.** @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with {@code key}, or* {@code null} if there was no mapping for {@code key}* @throws NullPointerException if the specified key or value is null*/public V put(K key, V value) {return putVal(key, value, false);}/** Implementation for put and putIfAbsent */final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());// 省略其他}
那么,為什么不支持 key 和 value 為 null 呢?
據(jù)查閱資料,ConcurrentHashMap
的作者 Doug Lea 自己的描述:
The main reason that nulls aren’t allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can’t be accommodated. The main one is that if map.get(key) returns null, you can’t detect whether the key explicitly maps to null vs the key isn’t mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.
可知 ConcurrentHashMap
是線程安全的容器,如果 ConcurrentHashMap
允許存放 null 值,那么當一個線程調(diào)用 get(key)
方法時,返回 null 可能有兩種情況:
(1) 一種是這個 key 不存在于 map 中
(2) 另一種是這個 key 存在于 map 中,但是它的值為 null。
這樣就會導(dǎo)致線程無法判斷這個 null 是什么意思。
在非并發(fā)的場景下,可以通過 map.contains(key)
檢查是否包括該 key,從而斷定是不存在 key 還是存在key 但值為 null,但是在并發(fā)場景下,判斷后調(diào)用其他 api 之間 map 的數(shù)據(jù)已經(jīng)發(fā)生了變化,無法保證對同一個 key 操作的一致性。
2.3 怎么解決?
2.3.1 封裝 put 方法,使用前判斷
建議封裝 put 方法,統(tǒng)一使用該方法對 ConcurrentHashMap
的 put 操作進行封裝,當 value 為 null 時,直接 return 即可。
Map<String, Person> map = new ConcurrentHashMap<>();// 封裝 put 操作,為 null 時返回
private void putPerson(String key, Person value){if(value == null){return;}map.put(key, value);
}
2.3.2 使用 Optional 類型
使用 Optional
// 創(chuàng)建一個 ConcurrentHashMap<String, Optional<String>>
Map<String, Optional<String>> map = new ConcurrentHashMap<>();// 插入或更新 key-value 對
map.computeIfAbsent("name", k -> Optional.ofNullable("Alice")); // 如果 name 不存在,則插入 ("name", Optional.of("Alice"))
map.computeIfAbsent("age", k -> Optional.ofNullable(null)); // 如果 age 不存在,則插入 ("age", Optional.empty())// 獲取 value
Optional<String> name = map.get("name"); // 返回 Optional.of("Alice")
Optional<String> age = map.get("age"); // 返回 Optional.empty()
Optional<String> gender = map.get("gender"); // 返回 null
2.3.3 自定義一個表示 null 的類
自定義表示 null 的類, 然后對 put 和 get 操作進行二次封裝,參考代碼如下:
// 定義一個表示 null 的類
public class NullValue extends Person{}// 創(chuàng)建一個 ConcurrentHashMap<String, Object>
private Map<String, Person> map = new ConcurrentHashMap<>();private static final NullValue nullValue = new NullValue();//使用示例: 值不為 null 時
putPerson("1002", new Person("張三"));//使用示例: 值為 null 時
putPerson("1003", null);// 封裝設(shè)置操作
private void putPerson(String key,Person person){if(person == null){map.put(key, nullValue);return;}map.put(key, person);
}// 封裝獲取操作
private Person getPerson(String key){if(key == null){return;}Person person = map.get(key);if(person instanceof NullValue){return null;}return person;
}
2.3.4 使用其他線程安全的 Java Map 類
Java 中也有支持 key 和 value 為 null 的線程安全的集合類,比如 ConcurrentSkipListMap (JDK) 和 CopyOnWriteMap (三方)。
ConcurrentSkipListMap
是一個基于跳表的線程安全的 map,它使用鎖分段的技術(shù)來提高并發(fā)性能。它允許 key 和 value 為 null,但是它要求 key 必須實現(xiàn)Comparable
接口或者提供一個Comparator
。CopyOnWriteMap
是一個基于數(shù)組的線程安全的 map,它使用寫時復(fù)制的策略來保證并發(fā)訪問的正確性。它允許 key 和 value 為 null。
注意 JDK 中沒有提供 CopyOnWriteMap
,很多三方類庫提供了對應(yīng)的工具類。如org.apache.kafka.common.utils.CopyOnWriteMap
。
2.4 常見的 Map
接口的子類的使用場景
Map 接口有很多子類,那么他們各自的適用場景是怎樣的呢?
使用場景主要取決于以下幾個方面:
- 是否需要線程安全:如果需要在多線程環(huán)境下操作
Map
,那么應(yīng)該使用ConcurrentHashMap
、ConcurrentSkipListMap
,它們都是并發(fā)安全的。而HashMap
、TreeMap
、HashTable
和LinkedHashMap
則不是,并且HashTable
已經(jīng)被ConcurrentHashMap
取代。 - 是否需要保證鍵的順序:如果需要按照鍵的自然順序或者插入順序遍歷
Map
,那么應(yīng)該使用TreeMap
或者LinkedHashMap
,它們都是有序的。而ConcurrentSkipListMap
也是有序的,并且支持范圍查詢。其他類則是無序的。 - 是否需要高效地訪問和修改:如果需要快速地獲取和更新
Map
中的元素,那么應(yīng)該使用HashMap
或者ConcurrentHashMap
,它們都是基于散列函數(shù)實現(xiàn)的,具有較高的性能。
而TreeMap
和ConcurrentSkipListMap
則是基于平衡樹實現(xiàn)的,具有較低的性能。CopyOnWriteMap
則是基于數(shù)組實現(xiàn)的,并發(fā)寫操作會復(fù)制整個數(shù)組,因此寫操作開銷很大。
在選擇合適的 Map
接口實現(xiàn)時,需要根據(jù)具體需求和場景進行權(quán)衡。
三、總結(jié)
基本功很重要,有時候基本功不扎實,更容易遇到一些奇奇怪怪的坑。假設(shè)你不了解 ConcurrentHashMap
的 kv 不能為 null, 測試的時候沒有覆蓋這種場景,等上線以后遇到這個問題可能直接導(dǎo)致線上問題,甚至線上故障。
ConcurrentHashMap
作者在 put 方法注釋中給出了 kv 不允許為 null 的提示,并沒有在注釋中給出設(shè)計原因,給眾多讀者帶來了諸多困惑。這也給我們很大的啟發(fā),當我們的某些設(shè)計容易引起別人的困惑和好奇時,不僅要將注意事項放在注釋中,更應(yīng)該將設(shè)計原因放在注釋里,避免給使用者帶來困擾。
“適合自己的才是最好的”。正如不同的 Map
實現(xiàn)類各有千秋,使用場景各有不同,我們需要根據(jù)具體需求和場景進行權(quán)衡一樣,我們在設(shè)計方案時也會遇到類似的場景,我們能做的是根據(jù)場景選擇最適合的方案。
我們遇到的任何問題,都是徹底掌握某個知識的絕佳機會。當我們遇到問題時,應(yīng)該主動掌握相關(guān)知識,希望大家不僅能夠知其然,還要知其所以然。
創(chuàng)作不易,如果本文對你有幫助,歡迎點贊、收藏加關(guān)注,你的支持和鼓勵,是我創(chuàng)作的最大動力。