从源码看Java HashMap原理

Java中HashMap是常用的容器类,对其原理性的东西一直停留在道听途说的程度,但是对细节以及优化的缘由方面一直没有进行过深入的思考。考虑到后期将会为自己前途奔波操劳(^.^),决定打开源码粗略研究一番。

由于HashMap的源码很长,不可能对每一个细节逐一深钻,尤其是涉及到处理hash冲突的红黑树优化,对于红黑树具体实现不做研究。主要是为了分清主次矛盾,避免深入细节而不能自拔,毕竟红黑树方面目前知道原理,在对照规则前提下,能撸出一二,同时知道添加删除原理方面,在日常开发和对知识的理解上已经足够。具体对于红黑树的具体操作,日后可能会有新的文章。

本文主要关注的几点:

  1. 如何计算hash,以及这样计算hash的原因
  2. hash,key,value如何存储
  3. 如何触发红黑树优化,以及为何要这么优化
  4. resize过程经历了什么

如何计算hash,以及这样计算hash的原因

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

算法很简单,就是利用key的hashCode的返回结果,高16位和低16位亦或的结果作为key的hash值的低16位,而高16位保持不变。这样做的目的是,当计算key的index时,通过 (n-1)&hash 的方式(n为数组长度)计算出来的index很容易发生碰撞,为了降低这种碰撞带来的影响,设计者通过高低16位异或的方式来抵消掉部分影响。

即使真的碰撞很严重时,存储方式也从链表转向了红黑树。

另一个问题是,为什么数组的长度都是2的指数倍?

原因是利于计算hash在数组中的下标。只有当n为2的指数倍时,n-1才会在低于m位上全为1,从而hash的低m位刚好是下标位置。其中m为n-1的二进制位数。

hash,key,value如何存储

首先,HashMap有一个成员变量叫table:

	transient Node<K,V>[] table;

这个table存储key和value的键值对Node。这个Node比较简单,主要存储key,value,hash,以及指向下一个Node的next引用。由此可见,Node会作为一个链表,头结点存在table的某个位置上。

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        ...
    }

还有另外一种结点:TreeNode。这个TreeNode是一个红黑树结点,但是最终又会继承自Node。

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        ...
   }

对于插入结点时,如果table没有初始化,或者没有插入的结点没有发生冲突的情况,就会在putVal中直接将Node结点放在table的某个index上,这个index的位置为(n - 1) & hash,刚才已经分析过,由于n是2的指数倍,(n - 1) & hash会计算出hash在数组中的index。

    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);

如果发生了冲突,就会有3种情况:

  1. key值相等,就会覆盖掉之前的Node!
  2. 如果数组中当前slot为红黑树TreeNode结点,说明当前slot已经被调整成红黑树而非链表的结构,具体如何调整在resize时说明。那么就按照红黑树的方式进行树插入操作。
  3. 剩下的情况就是slot存储的是Node结点,说明当前是链表。那么就沿着链表检查是否已经包含待插入的key,有的话覆盖;没有的话在链表末尾插入待插入的Node结点。只是需要检查当前slot中Node的数量是否已经超过了TREEIFY_THRESHOLD值。TREEIFY_THRESHOLD默认为8。TREEIFY_THRESHOLD是决定是否将链表调整成红黑树的阈值。因为当链表过长时,当前slot冲突很严重,所以通过红黑树的结构,可以将查找的时间复杂度从o(n)降到o(logn)。具体调整的过程在treeifyBin中实现。
    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;
        }
    }

如何触发红黑树优化,以及为何要这么优化

当某个hash对应的slot的链表长度超过TREEIFY_THRESHOLD=8时,就会触发treeifyBin。

treeifyBin中,只有当table的长度大于等于MIN_TREEIFY_CAPACITY(默认64)时,才会触发红黑树调整。也就是说,对于红黑树的调整要满足2个条件:

  1. slot的链表长度大于TREEIFY_THRESHOLD
  2. table的长度大于MIN_TREEIFY_CAPACITY

这样做的目的是,当table的长度比较小时,不宜于进行红黑树调整,因为红黑树的插入操作也将花费o(logn)的时间,这时最好对table进行扩容,即长度加长。只有当table长度大于MIN_TREEIFY_CAPACITY时,一味地对table长度进行扩容会造成table中有过的“缝隙”,造成空间浪费。这时,提高冲突时查找key的效率,可以将查找的时间复杂度从o(n)降到o(logn)就显得比较划算了。

resize过程经历了什么

偷个懒,看注释:

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {...}

由于table是2的指数倍,所以在扩容时候选择double式的扩容方式,对table的长度乘以2。由于table的长度扩大了一倍,每个slot中的Node所在的slot都需要不移动或者移动2的指数倍,从而使得slot的链表长度小于TREEIFY_THRESHOLD。问题是,具体应该把每一个Node挪到哪个位置的问题。其实可以根据当前table长度的二进制位数,重新计算所在的slot。比如之前是hash的后m位表示所在的slot,由于扩容了一倍,所以当前的slot位置应为结点的后m+1所表示的数值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值