前言:
记得初学 Java Collections Framework时,每次需要记一些如Hashtable不能存储null,HashMap能存储,还有List和Set能不能存储null,这在我初学阶段都是得死记硬背的,最近看了下集合框架的一些源码加上网上各路大神的看法,所以总结了下,如有误,请及时指正。
看看Map
看看HashMap的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里put时需要计算hash()方法,当key为null时,hash()直接返回0,不会调用key。hashCode()方法。不会产生空指针报错。不过HashMap的key只能存储一个null,因为key是不能重复的,所以有且只能有一个null。
注意:HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 个桶的位置,因为这个 Key 无法计算 hashCode(),因此只能规定一个桶让它存放。
下面看看Hashtable
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
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;
}
在方法开头也对value进行了null的判断,如果为空直接抛出空指针异常。而且这里没有管是否key为null,所以在int hash = key.hashCode();
这行中如果key为null,会报空指针异常。从源码直接看到Hashtable不能存储null的key和value。
看看TreeMap
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
还有这段代码中的compare()方法:
@SuppressWarnings("unchecked")
final int compare(Object k1, Object k2) {
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
}
可以看到key为空时会抛出空指针异常,这里没有直接看出value是否能为空,但是通过自己写单元测试后发现是可以存储value为null的值的。还有从compare方法中的compreTo方法可以看出key为null也会抛出空指针异常。
接下来看看List
看看ArrayList的源码:
public boolean add(E e) {
// 看是否需要扩容,需要则进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这里是没有对是否为null有所要求的,且List底层为Object数组。无论存不存储null是没有影响的。而对于LinkedList,底层为双向链表,节点的值node.value=null
是没有影响的。
对于Vector,和ArrayList源码很相似,add方法几乎相同也没有对null的限制。
Set
由于HashSet的底层是HashMap,Set存储的值都是在key中,所以可以看出Set不能存储重复值的原因就来自于Key不能重复,由于是HashMap,所以key是可以为null的即HashSet有且仅有一个null值。
而对于TreeSet,其底层也是TreeMap,所以对于null的限制也同TreeMap,这里就不多说了。
为什么Hashtable键和值都设计成不能为null
参考https://www.jianshu.com/p/c32e192e371d
中的引自牛客网的大佬的解释:
因为Hashtable,ConcurrentHashMap它们是用于多线程的,并发的,如果map.get(key)得到了null ,不能判断到底是映射的value是nll,还是因为没有找到对应的key而为空,而用于单线程状态的Hashmap却可以用containsKey ( key )去判断到底是否包含了这个null。而为什么Hashtable不能用containsKey()方法呢?因为要考虑到原子性问题:一个线程先get(key)再containsKey(key),这两个方法的中间时刻,其他线程怎么操作这个key都会可能发生,例如删掉这个key。就不能保证操作的原子性了。