為什么ConcurrentHashMap不允許插null?
在 Java 語(yǔ)言中,ConcurrentHashMap 和 Hashtable 這些線程安全的集合是不允許 key 或 value 插入 null 值的,而 HashMap 又允許 key 或 value 插入 null 值,這到底是為什么呢?
null 值插入演示
首先給 HashMap 插入 null 值,實(shí)現(xiàn)代碼如下:
HashMap<String, Object> map = new HashMap();
// 插入 null 值
map.put(null, null);
if (map.containsKey(null)) {
System.out.println("存在 null");
} else {
System.out.println("不存在 null");
}
以上程序的執(zhí)行結(jié)果如下:
圖片
從上述結(jié)果可以看出,HashMap 是允許 key 或 value 插入 null 值的。接著我們使用同樣的方式嘗試給 ConcurrentHashMap 的 key 和 value 插入 null 值,實(shí)現(xiàn)代碼如下:
圖片
編譯階段沒(méi)有報(bào)錯(cuò),執(zhí)行以上程序,得到的結(jié)果如下:
圖片
從上述報(bào)錯(cuò)信息可以看出,使用 ConcurrentHashMap 是不能插入 null 值的,否者程序在運(yùn)行期間就會(huì)報(bào)空指針異常。
PS:Hashtable 使用與 ConcurrentHashMap 類似,這里就不再重復(fù)演示了。
ConcurrentHashMap 源碼分析
為了尋找報(bào)錯(cuò)的原因,我們嘗試打開(kāi) ConcurrentHashMap 的源碼一探究竟。打開(kāi) ConcurrentHashMap 添加元素的方法 put 實(shí)現(xiàn)源碼如下:
圖片
從上述源碼可以看出,在添加方法的第一句就加了判斷:如果 key 值為 null 或者是 value 值為 null,就直接拋出異常 NullPointerException 空指針異常,這就是咱們前面程序報(bào)錯(cuò)的原因了。
探索最終原因
通過(guò)上面源碼分析,我們似乎已經(jīng)找到了 ConcurrentHashMap 不允許插入 null 值的原因,用一句話概括就是:烏龜?shù)钠ü伞耙?guī)定”!然而,這個(gè)原因是不能說(shuō)服面試官的,雖然源碼是這樣設(shè)計(jì)的,但我們要思考的是,這樣設(shè)計(jì)背后更深層次的原因,為什么 ConcurrentHashMap 不允許插入 null?而 HashMap 又允許插入 null 呢?
二義性問(wèn)題
所謂的二義性問(wèn)題是指含義不清或不明確。我們假設(shè) ConcurrentHashMap 允許插入 null,那么此時(shí)就會(huì)有二義性問(wèn)題,它的二義性含義有兩個(gè):
- 值沒(méi)有在集合中,所以返回 null。
- 值就是 null,所以返回的就是它原本的 null 值。
可以看出這就是 ConcurrentHashMap 的二義性問(wèn)題,那為什么 HashMap 就不怕二義性問(wèn)題呢?
可證偽的 HashMap
上面說(shuō)到 HashMap 是不怕二義性問(wèn)題的,為什么呢?這是因?yàn)?HashMap 的設(shè)計(jì)是給單線程使用的,所以如果查詢到了 null 值,我們可以通過(guò) hashMap.containsKey(key) 的方法來(lái)區(qū)分這個(gè) null 值到底是存入的 null?還是壓根不存在的 null?這樣二義性問(wèn)題就得到了解決,所以 HashMap 不怕二義性問(wèn)題。
不可證偽的 ConcurrentHashMap
而 ConcurrentHashMap 就不一樣了,因?yàn)?ConcurrentHashMap 使用的場(chǎng)景是多線程,所以它的情況更加復(fù)雜。我們假設(shè) ConcurrentHashMap 可以存入 null 值,有這樣一個(gè)場(chǎng)景,現(xiàn)在有一個(gè)線程 A 調(diào)用了 concurrentHashMap.containsKey(key),我們期望返回的結(jié)果是 false,但在我們調(diào)用 concurrentHashMap.containsKey(key) 之后,未返回結(jié)果之前,線程 B 又調(diào)用了 concurrentHashMap.put(key,null) 存入了 null 值,那么線程 A 最終返回的結(jié)果就是 true 了,這個(gè)結(jié)果和我們之前預(yù)想的 false 完全不一樣。也就是說(shuō),多線程的狀況非常復(fù)雜,我們沒(méi)辦法判斷某一個(gè)時(shí)刻返回的 null 值,到底是值為 null,還是壓根就不存在,也就是二義性問(wèn)題不可被證偽,所以 ConcurrentHashMap 才會(huì)在源碼中這樣設(shè)計(jì),直接杜絕 key 或 value 為 null 的歧義問(wèn)題。
ConcurrentHashMap 設(shè)計(jì)者的回答
對(duì)于 ConcurrentHashMap 不允許插入 null 值的問(wèn)題,有人問(wèn)過(guò) ConcurrentHashMap 的作者 Doug Lea,以下是他回復(fù)的郵件內(nèi)容:
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. Further digressing: I personally think that allowing nulls in Maps (also Sets) is an open invitation for programs to contain errors that remain undetected until they break at just the wrong time. (Whether to allow nulls even in non-concurrent Maps/Sets is one of the few design issues surrounding Collections that Josh Bloch and I have long disagreed about.)
It is very difficult to check for null keys and values in my entire application .
Would it be easier to declare somewhere static final Object NULL = new Object(); and replace all use of nulls in uses of maps with NULL? -Doug
以上信件的主要意思是,Doug Lea 認(rèn)為這樣設(shè)計(jì)最主要的原因是:不容忍在并發(fā)場(chǎng)景下出現(xiàn)歧義!
總結(jié)
在 Java 語(yǔ)言中,HashMap 這種單線程下使用的集合是可以設(shè)置 null 值的,而并發(fā)集合如 ConcurrentHashMap 或 Hashtable 是不允許給 key 或 value 設(shè)置 null 值的,這是 JDK 源碼層面直接實(shí)現(xiàn)的,這樣設(shè)計(jì)的目的主要是為了防止并發(fā)場(chǎng)景下的歧義問(wèn)題。
參考文檔
cnblogs.com/fanguangdexiaoyuer/p/12335921.html