網(wǎng)站一般在哪建設(shè)網(wǎng)絡(luò)推廣人員
一、背景
最近設(shè)計(jì)某個(gè)類庫(kù)時(shí)使用了 ConcurrentHashMap
最后遇到了 value 為 null 時(shí)報(bào)了空指針異常的坑。
本文想探討下以下幾個(gè)問(wèn)題:
(1) Map
接口的常見子類的 kv 對(duì) null 的支持情況。
(2)為什么 ConcurrentHashMap
不支持 key 和 value 為 null?
(3)如果 value 可能為 null ,該如何處理?
(4)有哪些線程安全的 Java Map 類?
(5) 常見的 Map
接口的子類,如 HashMap
、TreeMap
、ConcurrentHashMap
、ConcurrentSkipListMap
的使用場(chǎng)景。
二、探究
2.1 Map
接口的常見子類的 kv 對(duì) null 的支持情況
下圖來(lái)源于孤盡老師 《碼出高效》 第 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 值,那么當(dāng)一個(gè)線程調(diào)用 get(key)
方法時(shí),返回 null 可能有兩種情況:
(1) 一種是這個(gè) key 不存在于 map 中
(2) 另一種是這個(gè) key 存在于 map 中,但是它的值為 null。
這樣就會(huì)導(dǎo)致線程無(wú)法判斷這個(gè) null 是什么意思。
在非并發(fā)的場(chǎng)景下,可以通過(guò) map.contains(key)
檢查是否包括該 key,從而斷定是不存在 key 還是存在key 但值為 null,但是在并發(fā)場(chǎng)景下,判斷后調(diào)用其他 api 之間 map 的數(shù)據(jù)已經(jīng)發(fā)生了變化,無(wú)法保證對(duì)同一個(gè) key 操作的一致性。
2.3 怎么解決?
2.3.1 封裝 put 方法,使用前判斷
建議封裝 put 方法,統(tǒng)一使用該方法對(duì) ConcurrentHashMap
的 put 操作進(jìn)行封裝,當(dāng) value 為 null 時(shí),直接 return 即可。
Map<String, Person> map = new ConcurrentHashMap<>();// 封裝 put 操作,為 null 時(shí)返回
private void putPerson(String key, Person value){if(value == null){return;}map.put(key, value);
}
2.3.2 使用 Optional 類型
使用 Optional
// 創(chuàng)建一個(gè) ConcurrentHashMap<String, Optional<String>>
Map<String, Optional<String>> map = new ConcurrentHashMap<>();// 插入或更新 key-value 對(duì)
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 自定義一個(gè)表示 null 的類
自定義表示 null 的類, 然后對(duì) put 和 get 操作進(jìn)行二次封裝,參考代碼如下:
// 定義一個(gè)表示 null 的類
public class NullValue extends Person{}// 創(chuàng)建一個(gè) ConcurrentHashMap<String, Object>
private Map<String, Person> map = new ConcurrentHashMap<>();private static final NullValue nullValue = new NullValue();//使用示例: 值不為 null 時(shí)
putPerson("1002", new Person("張三"));//使用示例: 值為 null 時(shí)
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
是一個(gè)基于跳表的線程安全的 map,它使用鎖分段的技術(shù)來(lái)提高并發(fā)性能。它允許 key 和 value 為 null,但是它要求 key 必須實(shí)現(xiàn)Comparable
接口或者提供一個(gè)Comparator
。CopyOnWriteMap
是一個(gè)基于數(shù)組的線程安全的 map,它使用寫時(shí)復(fù)制的策略來(lái)保證并發(fā)訪問(wèn)的正確性。它允許 key 和 value 為 null。
注意 JDK 中沒有提供 CopyOnWriteMap
,很多三方類庫(kù)提供了對(duì)應(yīng)的工具類。如org.apache.kafka.common.utils.CopyOnWriteMap
。
2.4 常見的 Map
接口的子類的使用場(chǎng)景
Map 接口有很多子類,那么他們各自的適用場(chǎng)景是怎樣的呢?
使用場(chǎng)景主要取決于以下幾個(gè)方面:
- 是否需要線程安全:如果需要在多線程環(huán)境下操作
Map
,那么應(yīng)該使用ConcurrentHashMap
、ConcurrentSkipListMap
,它們都是并發(fā)安全的。而HashMap
、TreeMap
、HashTable
和LinkedHashMap
則不是,并且HashTable
已經(jīng)被ConcurrentHashMap
取代。 - 是否需要保證鍵的順序:如果需要按照鍵的自然順序或者插入順序遍歷
Map
,那么應(yīng)該使用TreeMap
或者LinkedHashMap
,它們都是有序的。而ConcurrentSkipListMap
也是有序的,并且支持范圍查詢。其他類則是無(wú)序的。 - 是否需要高效地訪問(wèn)和修改:如果需要快速地獲取和更新
Map
中的元素,那么應(yīng)該使用HashMap
或者ConcurrentHashMap
,它們都是基于散列函數(shù)實(shí)現(xiàn)的,具有較高的性能。
而TreeMap
和ConcurrentSkipListMap
則是基于平衡樹實(shí)現(xiàn)的,具有較低的性能。CopyOnWriteMap
則是基于數(shù)組實(shí)現(xiàn)的,并發(fā)寫操作會(huì)復(fù)制整個(gè)數(shù)組,因此寫操作開銷很大。
在選擇合適的 Map
接口實(shí)現(xiàn)時(shí),需要根據(jù)具體需求和場(chǎng)景進(jìn)行權(quán)衡。
三、總結(jié)
基本功很重要,有時(shí)候基本功不扎實(shí),更容易遇到一些奇奇怪怪的坑。假設(shè)你不了解 ConcurrentHashMap
的 kv 不能為 null, 測(cè)試的時(shí)候沒有覆蓋這種場(chǎng)景,等上線以后遇到這個(gè)問(wèn)題可能直接導(dǎo)致線上問(wèn)題,甚至線上故障。
ConcurrentHashMap
作者在 put 方法注釋中給出了 kv 不允許為 null 的提示,并沒有在注釋中給出設(shè)計(jì)原因,給眾多讀者帶來(lái)了諸多困惑。這也給我們很大的啟發(fā),當(dāng)我們的某些設(shè)計(jì)容易引起別人的困惑和好奇時(shí),不僅要將注意事項(xiàng)放在注釋中,更應(yīng)該將設(shè)計(jì)原因放在注釋里,避免給使用者帶來(lái)困擾。
“適合自己的才是最好的”。正如不同的 Map
實(shí)現(xiàn)類各有千秋,使用場(chǎng)景各有不同,我們需要根據(jù)具體需求和場(chǎng)景進(jìn)行權(quán)衡一樣,我們?cè)谠O(shè)計(jì)方案時(shí)也會(huì)遇到類似的場(chǎng)景,我們能做的是根據(jù)場(chǎng)景選擇最適合的方案。
我們遇到的任何問(wèn)題,都是徹底掌握某個(gè)知識(shí)的絕佳機(jī)會(huì)。當(dāng)我們遇到問(wèn)題時(shí),應(yīng)該主動(dòng)掌握相關(guān)知識(shí),希望大家不僅能夠知其然,還要知其所以然。
創(chuàng)作不易,如果本文對(duì)你有幫助,歡迎點(diǎn)贊、收藏加關(guān)注,你的支持和鼓勵(lì),是我創(chuàng)作的最大動(dòng)力。