前面分析ConcurrentHashMap的过程中可以发现,其要求key和value不能为空。实际上,不仅仅是ConcurrentHashMap,前面的HashTable,以及ConcurrentSkipListMap,这些并发的Map都不允许为空。在面试的过程中,不少大厂也会拿这个问题做为追问的问题之一。那么我们就来具体聊聊为什么不能为null的深层次的原因。
层次1:源码不支持
是的,实际上确实是在源码上就没用提供支持。在hashtable中,对value进行了非空校验,而key,如果为空则会出现NullPointerException。
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
复制
key如果为空,则hashCode方法会出现空指针异常。
在ConcurrentHashMap中,和ConcurrentSkipListMap中,则分别进行了非空约束。
ConcurrentHashMap:
if (key == null || value == null) throw new NullPointerException();
复制
ConcurrentSkipListMap:
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
return doPut(key, value, false);
}
复制
而这个doPut方法中:
if (key == null)
throw new NullPointerException();
复制
从上面可以看出,在代码中直接就杜绝了使用null的可能性,只有HashMap是支持null的,但是是在put为空的时候,hash方法对null做了特殊处理,为null的时候hash值位0。
层次2:null会带来二义性
之所以并发的ConcurrentHashMap不支持null的深层次的原因在于,null会带来难以容忍的二义性。我们可以看看Doug Lea对这个问题的描述。
Handling Null Values in ConcurrentHashMap
Tutika Chakravarthy wrote:
> Hi ,
> I would like to replace some Hashmaps in our
> application, which are prone to multi threading issues
> with ConCurrentHashMap.
>
> Currently we keep null key and values in hashmap
> without any issues as HashMap allows them.
>
> But ConcurrentHashMap does not allow any null key and
> values .
>
Try to take Holger's advice. As mostly an aside though...
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
复制
原作者认为,在ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps)上,不允许null值的出现的主要原因是他可能会在并发的情况下带来难以容忍的二义性。如果在HashMap等非并发容器中,你可以通过contains方法来判断,这个key是究竟不存在,还是本来就是null。但是在并发容器中,如果允许空值的存在的话,你就没法判断真正的情况。用作者的话说就是:在Maps或者Sets集合中允许null值的存在,就是公开邀请错误进入你的程序。而这些错误,只有在发生错误的情况下才能被发现。
我们可以对HashMap进行测试:
public static void main(String[] args) {
HashMap map = new HashMap();
map.put(null,1);
System.out.println(map.containsKey(null));
map.put(1,null);
System.out.println(map.containsValue(null));
System.out.println(map.get(null));
}
复制
此时输出:
true
true
1
复制
可见,在HashMap之中,我们可以很容易的通过contains方法来判断key或者value为null是否真的存在。
但是这个问题要是出现在ConurrentMaps中了,那么就可能会有问题了。试想一下,当我们首先从map中get某个key,由于map中这个key不存在,那么会返回null,这之后我们通过contains进行判断,此时如果有线程并发写入了一条value为null的值,那么contains的结果就为true。这样就会与真实的情况不一致了,这就是二义性。
因此我们也需要注意Doug 的观点:不管容器是否考虑了线程安全问题,都不应该允许null值的出现。他觉得在现有的某些集合里面允许了null值的出现,是集合的设计问题。