写在开头
昨天在写《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成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!