HashMap 灵魂拷问:从源码找寻答案

前言

经常用 HashMap,本来以为没多少内容,但是仔细研究之后发现还是有点东西的。本文通过一些问题结合源码对 HashMap 进行记录,以便再次学习。

本文源码基于 JDK1.8

问题来了

问题1: HashMap 数据结构?

JDK 1.7 使用数组+链表,JDK1.8 使用数组+链表或红黑树结构。

HashMap 数据结构

问题2: HashMap 数据存放原理?

基本过程:

  1. 根据新增数据 HashCode 和 数组长度 确认下标,如果当前下标没有数据则直接存放。
  2. 如果当前下标有数据,则往该数据以链表结构往后添加。
  3. 当某个元素链表长度大于 8 之后,转换为红黑树。下次再往树结构下标添加数据时,添加到红黑树元素中。

源码分析:

HashMap # putVal()

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1.为空则初始化数组长度
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
     // 2. 如果当前下标元素链表为空,创建新结点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 当前下标元素链表存在值,则往链表后添加
        Node<K,V> e; K k;
        // 3. 如果 key 存在,覆盖 value
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4. 要添加的结点为树结点,则添加到树结构中
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.不是树则遍历链表
            for (int binCount = 0; ; ++binCount) {
                // 5.1 如果下一个结点为 null,则把新的结点添加即可
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 5.2 如果长度大于 8 放到数结构中
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 5.3 如果key相同,赋值给 e 跳出循环,后面进行值覆盖
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 6. 覆盖值操作
        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)
        // 7.扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}
  1. 当 HashMap 为空时,调用 resize() 方法初始化 HashMap 的容量。默认的初始容量为 DEFAULT_INITIAL_CAPACITY = 1 << 4 // 16 。
  2. 确定元素位置,创建新元素。

2.1 首先确定新元素的下标 i = (n - 1) & hash。n 是数组长度,那么 n-1 就是下标最大值。与 hash 码进行与运算,得出的结果必定是不大于最大下标的。

比如数组长度为 4,最大下标为 3 二进制为 00..011。hash 值转换为二进制为 01...10。
进行 & 运算由于下标前面都是补位的 0 ,& 运算之后都为 0。所以只有最后两位有意义。 11&10 = 10,下标为 2。
如果这时 hash 值的最后两位为 01,11&01=01 下标为 1。

2.2 确定好下标之后,调用 newNode 方法创建新结点并放置在下标位置。

  1. p 不为 null 说明当前下标存在元素。
    如果新增元素的 hash 以及 key 都与当前 p 元素一致,将 p 赋值给 e。
    到后面 6 中进行判断,将 e 的值覆盖为新的值,也就是说原来 p 的 hash key 的位置值被覆盖掉了。

  2. 如果结点是树类型的,把传入的信息生成一个新的树结点并存放到树结构中。

  3. 不是树类型,则遍历该元素链表进行处理。

  4. e 的值不为空,说明要传入的 key 已经存在了,最后把值覆盖掉。这就是 HashMap 定义值不能重复的代码实现。

  5. 如果数据达到了阈值,进行扩容。

问题3: HashMap 怎么扩容?

扩容条件:

存放完数据以后,判断当前容量是否达到阈值。一旦达到则需要进行扩容:

if (++size > threshold)
    resize();

基本过程:

  1. 容量翻倍;
  2. 重新放置元素。

源码分析(HashMap 不为空的情况下):

HashMap # resize()

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 达到最大值就不处理了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 1.左移一位进行翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 2.将所有结点放置在扩容表中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 3.链表结点没有下个元素,找到下标直接存放
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 4.树结构单独处理
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 5.运算之后为0,直接存放在当前下标
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 6.不为0,存放到当前下标+原来长度(oldCap)的位置
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 5.1不需要挪动的头结点
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 6.1需要挪动的头结点,放置在 j + oldCap 下标处
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

重点在于:

  • 进行遍历,如果某个元素不为链表或树结构,直接存放即可。因为原来它所在的下标就一个值,直接放在原位置即可。
  • 某个元素为链表,遍历链表中的结点。将 & 运算结果为 0 串起来,将结果不为 0 的串起来。最后将为 0 的一串放在原来下标位置,不为 0 的一串放在 [原来下标+原来长度] 的位置。为 0 的一串不需解释,放在原来位置。
    不为 0 的打个比方:原来长度是 4,下标为 1。数组扩容为两倍 8,那么需要放置在位置下标为 4+1=5 处。这样就比较均匀地分散了数据。

问题4: HashMap 初始容量大小?

默认容量是 16:

 /**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认负载因子:

 /**
  * The load factor used when none specified in constructor.
  */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;

如果创建 HashMap 时传入初始容量 k,则初始大小为距离 k 最近的 2 的整数次方。比如传入 10,经过计算为 16。

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

二进制右移并进行位或,结果会把所有位变为 1,最后再 +1 得到最接近的 2 的幂数整数。

问题5: HashMap 下标确认过程?

  1. 生成散列值:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

允许 key 为 null,返回 hash 码 0。反之,拿到该对象的 hashCode,然后将该 hashCode 的高 16 位与低 16 位进行异或(相同为 0 不同为 1)。

进行运算

为什么散列值这样生成?

高 16 位与低 16 位进行异或的算法叫做 扰动函数,这么做的好处有

  • 尽可能降低了 hash 碰撞;
  • 位或运算效率较高;
  • 变相地保留了 hashCode 的高 16 位。

为什么不直接使用 hashCode 作为散列值

先搞清楚下标生成规则:

  1. 将数组长度 -1 和 hash 值进行与运算(都想同为 1,否则为 0),就确定了下标。
Node<K,V> p;
int n = table.length;
p = tab[(n - 1) & hash];

打个比方,数组长度为 16,将某个 hash 值和 长度-1 进行与运算。因为只保留了后几位,所以得到的结果肯定不大于数组长度。

  10100101 11000100 00100101
& 00000000 00000000 00001111
--------------------------------
  00000000 00000000 00000101    //高位全部归零,只保留末四位

由于下标的生成主要在于数组长度的后几位,如果只是使用 hashCode 特容易发生碰撞。使用扰动函数处理过之后,加大了低位的随机性,减少碰撞。

这里也就说明了数组容量为 2 的整数幂的原因:要进行与运算确定下标。为 2 的整数幂,计算时减 1 确保后位全部为 1,这样与运算的时候会有很大的随机性。如果不是 2 的整数幂减 1,后位很多 0、运算的时候极易得到相同的下标,造成效率降低、内存浪费的后果。

问题6: HashMap 怎么获取元素?

基本过程:

  1. 根据 key 得到散列值,传入 key 进行查找。
  2. 找到直接返回,找不到遍历。

HashMap # get()

 public V get(Object key) {
     Node<K,V> e;
     // hash(key) 获得传入 key 的 hash 码
     return (e = getNode(hash(key), key)) == null ? null : e.value;
 }
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 1. 判空,如果 HashMap 为空直接返回 null
    // 数组长度和 hash 进行与运算,得到所查找元素下标,找不到同样返回 null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 2. 如果要查找 key 与第一个元素 hash 和 key 都相同,直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 3. 遍历查找后面的元素,找到就返回。找不到最后会返回 null
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

HashMap 查询元素时间复杂度

  • 根据上方查找元素的源码,如果能直接确认所在下标就是要查询的元素,直接返回。时间复杂度为 O(1)。
  • 如果需要遍历链表,时间复杂度为 O(n)。

以上仅为个人理解。

问题7: HashMap 遍历原理?

先来看一下 HashMap 遍历的方式:

HashMap<Object, Object> hashMap = new HashMap();
// 遍历key
for (Object k : hashMap.keySet()) {
}
// 遍历元素
for (HashMap.Entry<Object, Object> entry : hashMap.entrySet()) {
}

// 迭代器遍历key
Set<Object> keySet = hashMap.keySet();
Iterator<Object> iterator = keySet.iterator();
while (iterator.hasNext()){
    Object o = iterator.next();
}
// 迭代器遍历元素
Set<HashMap.Entry<Object, Object>> keySetI = hashMap.entrySet();
Iterator<HashMap.Entry<Object, Object>> iteratorI = keySetI.iterator();
while (iteratorI.hasNext()){
    HashMap.Entry<Object, Object> entry = iteratorI.next();
}

直接遍历

hashMap.keySet() 方法会返回一个 KeySet 集合:

HashMap # keySet()

    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

使用 for 循环即可遍历该 Set 集合,获取到 Key 的集合。

KeySet 是 HashMap 的一个内部类,创建时持有 HashMap 的第一个元素,有了开头的元素就可以执行遍历了。后面会详细记录它的实现。

迭代器遍历

keySet.iterator() 方法或获取到 HashMap 的迭代器:

final class KeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    ...
}

进行遍历用到迭代器的 next 方法

KeyIterator # next()

final class KeyIterator extends HashIterator
     implements Iterator<K> {
     public final K next() { return nextNode().key; }
 }

nextNode() 方法是父类 HashIterator 的,先看下 HashIterator 的构造函数:

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot
    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        // next 持有第一个元素
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }
    ...
}

注意成员变量 next 持有的 HashMap 的第一个元素的结点,再看 nextNode() 方法:

HashMap.HashIterator # nextNode()

final Node<K,V> nextNode() {
    Node<K,V>[] t;
    // 1. 把 next 结点复制给 e,从第一个元素开始查找
    Node<K,V> e = next;
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
     // 2.第一次调用 e 为初始结点,直接返回,并且 next 成为 e 的下一个结点。
     // 如果 e 的下一个结点为 null 并且表不为空,把表下一个元素赋值给 next。
    if ((next = (current = e).next) == null && (t = table) != null) {
        do {} while (index < t.length && (next = t[index++]) == null);
    }
    return e;
}

这样完成了每一个结点的遍历,至于 HashMap.Entry 的遍历与之类似,不再赘述。

HashMap 删除实现

HashMap 元素的移除就相对简单了,直接看源码。

HashMap # remove

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 判断数组是否为空,要移除的元素是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // key 对上了,node 记录要移除的元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // 没对上,说明可能在链表或红黑树中
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 遍历查找,直到找到了元素或查询完毕
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)// 树节点删除
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 如果是首节点,直接把下标位置指向下一个结点
                tab[index] = node.next;
            else // 如果不是,直接指向下一个结点,当前结点就删除了
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

总结

文中有关红黑树等内容暂时没有分析,等复习到数据结构再说吧。

参考资料:

一个HashMap跟面试官扯了半个小时
HashMap 源码详细分析(JDK1.8)

推荐阅读更多精彩内容