前言
Java 提供了丰富的容器类型,比如 Map 类 HashMap TreeMap,Set 类 HashSet TreeSet, List 类 ArrayList LinkedList 等,然而,这些容器类有些要求 key 不能为 null,有些要求 value 不能为 null,究其原因到底为何?本文记录自己对这些问题的一些思考和猜测,如有不对之处欢迎批评指正。
Map
先说现状
HashTableJDK1.0 引入,key 和 value 都不可为nullHashMapJDK1.2 引入,key 和 value 都可以为nullLinkedHashMapJDK1.4 引入,key 和 value 都可以为nullTreeMapJDK1.2 引入,key 不能为null,value 可以为nullConcurrentHashMapJDK1.5 引入,key 和 value 都不可为null
再说思考
在 JDK1.0 版本就引入了 HashTable,由于 HashTable 是线程安全的,可以多线程使用,因此如果 value 允许为 null,那么当需要判断是否包含某个 key 时,必然是调用 containsKey 方法来判断,当再调用 get 方法获取对应的 value 时,由于可以多线程使用该 HashTable,那么其他线程可能在此期间修改了对应的 key,导致返回值发生变化,可能出现不存在的情况。为应对这种情况,那么势必要通过加锁保证 containsKey 和 get 的调用是原子的,但这样一来用户使用的成本就增加了,接口的设计者肯定不期望这样的事情发生,故而不允许 value 为 null,那么通过判断返回值是否为 null 就可以得知是否存在对应的 key。
至于 HashTable key 不能为 null 暂时没有想到合理的解释,不过可以从如下的官方描述文档中猜测一二,设计者要求所有 key 都实现 hashCode 和 equals 方法,Java中只有非 null 的对象才实现了这两个方法。
This class implements a hash table, which maps keys to values. Any non-null object can be used as a key or as a value.
To successfully store and retrieve objects from a hashtable, the objects used as keys must implement the
hashCodemethod and theequalsmethod.
随后在 JDK1.2 版本中引入了 HashMap 和 TreeMap,这两个都不是线程安全的 Map,因此通常都是在单线程中操作,不用考虑线程安全问题。对于上述判断是否存在某个 key 的问题,就可以通过 containsKey 方法判断,然后通过 get 方法获取对应的 value,因此 value 可以为 null。
对于 HashMap 的 key 而言,设计者可能考虑 key 也并非不可为 null,只需对 null 的 key 特殊处理即可,代码中也确实如此(具体原因暂未想到合理解释)
对于 TreeMap 的 key 而言,无参构造的情况下 key 应实现 Comparable 接口,并通过 compareTo 方法对 key 进行排序,如果 key 为 null,则无法调用 compareTo 方法。
在 JDK1.4 版本中引入了 LinkedHashMap,这是一个维护了链表结构的 Map,可以用于构建 LRU 缓存,继承自 HashMap,因此其 key 和 value 都和 HashMap 的要求保持一致,都可以为 null。
在 JDK1.5 版本中引入了支持并发的、线程安全的 ConcurrentHashMap,从官方描述文档中可以看到是对标 HashTable 的,保持了统一的功能特性,因此 key 和 value 都不能为 null
A hash table supporting full concurrency of retrievals and high expected concurrency for updates. This class obeys the same functional specification as
java.util.Hashtable, and includes versions of methods corresponding to each method ofHashtable.
最后分析源码
HashTable
JDK1.0 开始引入 HashTable,是线程安全的,可以用于多线程场景
分析 put 操作
方法入口先对 value 判空,并创建对应的
Entry实例;调用 key 的hashCode方法获取 hash 值,要求 key 不能为null
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;
}
分析 get 操作
调用 key 的
hashCode方法计算 hash 值,如果不存在 key 对应的Entry实例则返回null,因此可以通过返回值是否为null判断是否存在对应的 key
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
HashMap
JDK2.0 开始引入HashMap
分析 put 操作
对于 key,先调用
hash方法计算 key 的 hash 值,如果key == null则取 0,否则取对应的hashCode方法参与计算;对于 value,在putVal时将 value 存储在内部类Node中,并没有特别的判空操作。
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);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; 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);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
分析 get 操作
如果 key 对应的内部类
Node实例不存在则返回null,这就意味着仅靠判断返回值是否为null无法判断Map中是否包含该元素
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
LinkedHashMap
JDK1.4 开始引入 LinkedHashMap,LinkedHashMap 继承自 HashMap
分析 get 操作
重写了父类的 get 方法,主要增加了
afterNodeAccess方法可以用于 LRU 缓存等
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
put 操作直接继承父类,不再分析
TreeMap
JDK1.2 开始引入 TreeMap,是一个有序 Map,使用自定义 Comparator 实现排序或者基于 key 的 Comparable#compareTo 方法来排序。
A Red-Black tree based
NavigableMapimplementation. The map is sorted according to theComparablenatural ordering of its keys, or by aComparatorprovided at map creation time, depending on which constructor is used.
分析 put 操作
如果构造时没有提供
Comparator实现,则要求 key 一定不能为null,否则无法调用Comparable#compareTo方法,因此统一考虑这两种方式,要求 key 不能为null。value 并没有判空操作,直接用于构造内部类Entry
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;
}
分析 get 操作
获取 key 对应的
Entry,如果获取不到则返回null,因此仅通过返回值是否为null无法判断是否存在该 key
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
ConcurrentHashMap
JDK1.5 开始引入 ConcurrentHashMap 是线程安全的,可以用于多线程场景
分析 put 操作
对 key 和 value 进行空校验,不能为空
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
// 省略以下内容
}
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
分析 get 操作
直接调用 key 的
hashCode方法获取 hash 值,如果不存在对应的 value 则直接返回null
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
Set
先说现状
HashSetJDK1.2 引入,元素可以为nullTreeSetJDK1.2 引入,元素不能为nullLinkedHashSetJDK1.4 引入,元素可以为null
再说思考
Set 底层是通过 Map 实现的,Map 的 key 组成了 Set,Map 的 value 是一个固定非 null 值,无需关注。
HashSet 对应 HashMap,由于 HashMap 的 key 允许为 null,所以 HashSet 元素可以为 null。
TreeSet 对应 TreeMap,由于 TreeMap 的 key 不允许为 null,所以 TreeSet 的元素不能为 null。
LinkedHashSet 对应 LinkedHashMap,由于 LinkedHashMap 的 key 允许为 null,所以 LinkedHashSet 的元素可以为 null。
最后分析源码
HashSet
HashSet 定义
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
}
分析 add 操作
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
分析 remove 操作
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
LinkedHashSet
LinkedHashSet 继承自 HashSet,add 和 remove 操作都和父类保持一致
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
/**
* Constructs a new, empty linked hash set with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity of the linked hash set
* @param loadFactor the load factor of the linked hash set
* @throws IllegalArgumentException if the initial capacity is less
* than zero, or if the load factor is nonpositive
*/
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
/**
* Constructs a new, empty linked hash set with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity of the LinkedHashSet
* @throws IllegalArgumentException if the initial capacity is less
* than zero
*/
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
/**
* Constructs a new, empty linked hash set with the default initial
* capacity (16) and load factor (0.75).
*/
public LinkedHashSet() {
super(16, .75f, true);
}
}
TreeSet
TreeSet 定义
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
/**
* The backing map.
*/
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a set backed by the specified navigable map.
*/
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
/**
* Constructs a new, empty tree set, sorted according to the
* natural ordering of its elements. All elements inserted into
* the set must implement the {@link Comparable} interface.
* Furthermore, all such elements must be <i>mutually
* comparable</i>: {@code e1.compareTo(e2)} must not throw a
* {@code ClassCastException} for any elements {@code e1} and
* {@code e2} in the set. If the user attempts to add an element
* to the set that violates this constraint (for example, the user
* attempts to add a string element to a set whose elements are
* integers), the {@code add} call will throw a
* {@code ClassCastException}.
*/
public TreeSet() {
this(new TreeMap<E,Object>());
}
/**
* Constructs a new, empty tree set, sorted according to the specified
* comparator. All elements inserted into the set must be <i>mutually
* comparable</i> by the specified comparator: {@code comparator.compare(e1,
* e2)} must not throw a {@code ClassCastException} for any elements
* {@code e1} and {@code e2} in the set. If the user attempts to add
* an element to the set that violates this constraint, the
* {@code add} call will throw a {@code ClassCastException}.
*
* @param comparator the comparator that will be used to order this set.
* If {@code null}, the {@linkplain Comparable natural
* ordering} of the elements will be used.
*/
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
分析 add 操作
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
分析 remove 操作
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
List
先说现状
Vector JDK1.0 引入,线程安全的 List,元素可以为 null
ArrayList JDK1.2 引入,非线程安全的 List,元素可以为 null
LinkedList JDK1.2 引入,非现场安全的 List,元素可以为 null
再说思考
List 同样在 JDK1.0 版本引入线程安全的容器类,但是 List 是有序序列容器,通常以索引来获取元素,只要索引没有越界,那么对应的元素一定存在,无需通过 null 判断是否存在某个元素,null 元素的引入不会引发歧义,因此元素都可以为 null。
最后分析源码
Vector
Vector 底层是数组
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* The array buffer into which the components of the vector are
* stored. The capacity of the vector is the length of this array buffer,
* and is at least large enough to contain all the vector's elements.
*
* <p>Any array elements following the last element in the Vector are null.
*
* @serial
*/
protected Object[] elementData;
}
分析 add 操作
不对添加的元素进行空校验,允许为
null
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
分析 get 操作
直接获取数组下标对应的元素值,也就意味着无法通过判断是否为
null来判断List中是否包含该元素。
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
如果需要判断是否包含某个元素,可以通过 contains 方法判断。
public boolean contains(Object o) {
return indexOf(o, 0) >= 0;
}
public synchronized int indexOf(Object o, int index) {
if (o == null) {
for (int i = index ; i < elementCount ; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = index ; i < elementCount ; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
ArrayList
ArrayList 底层是数组
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
}
分析 add 操作
不对添加的元素进行空校验,允许为
null
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
分析 get 操作
直接获取数组下标对应的元素值,也就意味着无法通过判断是否为
null来判断List中是否包含该元素。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
LinkedList
LinkedList 底层是链表结构
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
}
分析 add 操作
不对添加的元素进行空校验,允许为
null
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
分析 get 操作
直接获取数组下标对应的元素值,也就意味着无法通过判断是否为
null来判断List中是否包含该元素。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}