為什麼HashMap的鍵值可以為null,而ConcurrentHashMap不行?

JavaBuild發表於2024-03-03

寫在開頭

昨天在寫《HashMap很美好,但執行緒不安全怎麼辦?ConcurrentHashMap告訴你答案!》這篇文章的時候,漏了一個知識點,直到晚上吃飯的時候才突然想到,關於ConcurrentHashMap在儲存Key與Value的時候,是否可以存null的問題,按理說這是一個小問題,但build哥卻不敢忽視,尤其在現在很多面試官都極具挑剔的環境下,萬一同學們刷到了咱的部落格,回答中遺漏了這個小細節,錯過了面試官的考驗,那咱可就成罪人了。
接下來我們就將HashMap、Hashtable、ConcurrentHashMap這三集合類的鍵值是否可以null的問題,放一起對比去學習一下。

Hashtable的鍵值與null

雖然我們在講解HashMap與Hashtable作對比時,已經說了Hashtable在儲存key與value時均不可為null,但當時的側重點全在HashMap身上,就沒有詳細的解釋原因,下面我們跟進put原始碼中去一探緣由。

【原始碼解析1】

public synchronized V put(K key, V value) {
        // 確認值不為空
        if (value == null) {
            throw new NullPointerException(); // 如果值為null,則丟擲空指標異常
        }
 
        // 確認值之前不存在Hashtable裡
        Entry<?,?> tab[] = table;
        int hash = key.hashCode(); // 如果key如果為null,呼叫這個方法會丟擲空指標異常
        int index = (hash & 0x7FFFFFFF) % tab.length;//計算儲存位置
 
        //遍歷,看是否鍵或值對是否已經存在,如果已經存在返回舊值
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
 
        addEntry(hash, key, value, index);
        return null;
    }

透過Hashtable的put底層原始碼,我們可以看到,方法體內,首先就對value值進行的判空操作,如果為空則丟擲空指標異常;其次在計算hash值的時候,直接呼叫key的hashCode()方法,若keynull,自然也會報空指標異常,因此,我們在呼叫put方法儲存鍵值對時,key與value都非null。

HashMap的鍵值與null

我們同樣也透過HashMap的put方法去分析它的底層原始碼,先上程式碼。

【原始碼解析2-hash()】

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在計算hash值的時候,hashmap中透過三目運算子做了空值處理,直接返回0,這樣最終計算出key應該儲存在陣列的第一位上,且key是唯一性呢,因此,key最多存一個null;

【原始碼解析3】

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // 陣列
    HashMap.Node<K,V>[] tab; 
    // 元素
    HashMap.Node<K,V> p; 

    // n 為陣列的長度 i 為下標
    int n, i;
    // 陣列為空的時候
    if ((tab = table) == null || (n = tab.length) == 0)
        // 第一次擴容後的陣列長度
        n = (tab = resize()).length;
    // 計算節點的插入位置,如果該位置為空,則新建一個節點插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    ///
}

迴歸putVal()方法,我們逐句閱讀後也沒有發現對於value值為null的處理與限定,因此,它可以儲存為null的value值,我們知道HashMap的鍵值對特點如同身份證與人名一樣,key等同於身份證,全國唯一,而value值等同於人名,可以重複,比如全國有上萬個叫張偉的,所以value值也就同樣允許儲存多個null。

ConcurrentHashMap的鍵值與null

很多同學們可能會以為ConcurrentHashMap不過是HashMap在多執行緒環境下的版本,底層實現都一致,只是多了加鎖的操作,所以二者對於null的允許程度是一樣。
如果你是這樣想,那可就完全錯了,對於ConcurrentHashMap來說,它也不允許儲存鍵值對為null的資料。
Doug Lea(ConcurrentHashMap的設計者)曾這樣說道:

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.

大致的意思是,在單執行緒環境中,不會存在一個執行緒操作該 HashMap 時,其他的執行緒將該 HashMap 修改的情況,可以透過 contains(key)來做判斷是否存在這個鍵值對,從而做相應的處理;
而在多執行緒環境下,可能會存在多個執行緒同時修改鍵值對的情況,這時是無法透過contains(key)來判斷鍵值對是否存在的,這會帶來一個二義性的問題,Doug Lea說二義性是多執行緒中不能容忍的!

啥是二義性? 咱們通俗點講就是一個結果,2種釋義,就好比我們透過get方法獲取值的時候,返回一個null,其實我們是無法判斷是值本身為null還是說集合中就沒這個值!

所以說,ConcurrentHashMap的key和value均不可為null。

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

如果您想與Build哥的關係更近一步,還可以關注俺滴公眾號“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

相關文章