为什么HashMap的键值可以为null,而ConcurrentHashMap不行?

写在开头

昨天在写《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。

posted on 2024-06-25 16:24  myf008  阅读(5)  评论(0编辑  收藏  举报

导航