关于Java 容器中 null 元素的思考

1,039 阅读10分钟

前言

Java 提供了丰富的容器类型,比如 MapHashMap TreeMapSetHashSet TreeSet, ListArrayList LinkedList 等,然而,这些容器类有些要求 key 不能为 null,有些要求 value 不能为 null,究其原因到底为何?本文记录自己对这些问题的一些思考和猜测,如有不对之处欢迎批评指正。

Map

先说现状

  • HashTable JDK1.0 引入,key 和 value 都不可为 null
  • HashMap JDK1.2 引入,key 和 value 都可以为 null
  • LinkedHashMap JDK1.4 引入,key 和 value 都可以为 null
  • TreeMap JDK1.2 引入,key 不能为 null,value 可以为 null
  • ConcurrentHashMap JDK1.5 引入,key 和 value 都不可为 null

再说思考

在 JDK1.0 版本就引入了 HashTable,由于 HashTable 是线程安全的,可以多线程使用,因此如果 value 允许为 null,那么当需要判断是否包含某个 key 时,必然是调用 containsKey 方法来判断,当再调用 get 方法获取对应的 value 时,由于可以多线程使用该 HashTable,那么其他线程可能在此期间修改了对应的 key,导致返回值发生变化,可能出现不存在的情况。为应对这种情况,那么势必要通过加锁保证 containsKeyget 的调用是原子的,但这样一来用户使用的成本就增加了,接口的设计者肯定不期望这样的事情发生,故而不允许 value 为 null,那么通过判断返回值是否为 null 就可以得知是否存在对应的 key。

至于 HashTable key 不能为 null 暂时没有想到合理的解释,不过可以从如下的官方描述文档中猜测一二,设计者要求所有 key 都实现 hashCodeequals 方法,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 hashCode method and the equals method.

随后在 JDK1.2 版本中引入了 HashMapTreeMap,这两个都不是线程安全的 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 of Hashtable.

最后分析源码

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 开始引入 LinkedHashMapLinkedHashMap 继承自 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 NavigableMap implementation. The map is sorted according to the Comparable natural ordering of its keys, or by a Comparator provided 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

先说现状

  • HashSet JDK1.2 引入,元素可以为 null
  • TreeSet JDK1.2 引入,元素不能为 null
  • LinkedHashSet JDK1.4 引入,元素可以为 null

再说思考

Set 底层是通过 Map 实现的,Map 的 key 组成了 SetMap 的 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];
}