jdk1.8之HashMap底层原理

通俗概括

可以通俗地理解HashMap是一个存储元素为链表的数组。首先,用户调用put(key,value)方法存储一对键值对的时候。会先调用key.hash()获取key的哈希值,通过这个哈希值确定这对键值对应该存储在数组中的位置,如果这个位置上已经存储有元素了,就说明发生了哈希冲突了。这时候就是通过将新的键值对放在旧的键值对后面,形成链表。如果链表过长的话,会装换成红黑树以便提高元素的查找效率。同时,hashmap的扩容因子是0.75,初始容量是16,就是当该hashmap中的元素总数大于 现容量x扩容因子 的时候,会发生扩容,即将数组扩容为原来的2倍。

源码分析

put函数

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

hash函数

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

hash函数是对object中的hashCode函数的二次封装
跟踪到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;
       	//判断table数组是否为null或者是否长度为0,是就调用resize()来扩容
        if ((tab = table) == null || (n = tab.length) == 0)
        //n为扩容后tab数组的长度,resize()是对数组进行扩容
            n = (tab = resize()).length;
        //元素存储位置是(n-1)&hash,查看该位置上是否已经有元素
        if ((p = tab[i = (n - 1) & hash]) == null)
        //没有元素的话,直接存放新的元素
            tab[i] = newNode(hash, key, value, null);
        else {
        //有元素的话,分情况考虑
            Node<K,V> e; K k;
            //hash一样、key存储地址一样、equals为true,说明要更新value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                
                e = p;
            //判断是否是一个TreeNode
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //发生hash冲突了,hashmap是通过链表法解决的
            else {
            	//通过循环找到链表中最后一个节点,将新的元素插入到链表的最后
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //>=8-1个节点的时候,转换成红黑树
                        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;
                }
            }
            //e不为null,说明该key已经存在于hashmap中了,修改e对应的value就可以了
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //修改计数器+1
        ++modCount;
        //判断当前的容量是否需要扩容
        if (++size > threshold)
            resize();//扩容
        //回调函数,不用管
        afterNodeInsertion(evict);
        return null;
    }

看一下table数组的定义

transient Node<K,V>[] table;

table数组就是hashmap中存储存储链表头结点的数组了,它存储的元素是一个Node节点。
看一下Node节点,就是一个链表的普通节点

   static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//key的hash值
        final K key;//key值
        V value;//value值
        Node<K,V> next;//指向下一个node的指针

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

到这里,大概也清晰了整个HashMap的流程,因为篇幅问题,没有对其他函数进行分析。
以下,补充一些知识点:
1、为什么HashMap不直接用object中的hashCode函数来计算哈希值呢,而是用了自己对hashCode函数的二次封装的hash函数作为哈希值呢?
我们确定存储数据位置的一行代码:

first = tab[(n - 1) & hash])

使用数组长度减一 与运算 hash 值。这行代码就是为什么要让前面的 hash 方法移位并异或。

我们分析一下:

首先,假设有一种情况,对象 A 的 hashCode 为 1000010001110001000001111000000,对象 B 的 hashCode 为 0111011100111000101000010100000。

如果数组长度是16,也就是 15 与运算这两个数, 你会发现结果都是0。这样的散列结果太让人失望了。很明显不是一个好的散列算法。

但是如果我们将 hashCode 值右移 16 位,也就是取 int 类型的一半,刚好将该二进制数对半切开。并且使用位异或运算(如果两个数对应的位置相反,则结果为1,反之为0),这样的话,就能避免我们上面的情况的发生。

总的来说,使用位移 16 位和 异或 就是防止这种极端情况。但是,该方法在一些极端情况下还是有问题,比如:10000000000000000000000000 和 10000000001000000000000000 这两个数,如果数组长度是16,那么即使右移16位,在异或,hash 值还是会重复。但是为了性能,对这种极端情况,JDK 的作者选择了性能。毕竟这是少数情况,为了这种情况去增加 hash 时间,性价比不高。

2、HashMap 为什么使用 & 与运算代替模运算?
我们再看看刚刚说的那个根据hash计算下标的方法:

tab[(n - 1) & hash];

其中 n 是数组的长度。其实该算法的结果和模运算的结果是相同的。但是,对于现代的处理器来说,除法和求余数(模运算)是最慢的动作。

上面情况下和模运算相同

a % b == (b-1) & a ,当b是2的指数时,等式成立。

我们说 & 与运算的定义:与运算 第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0;

当 n 为 16 时, 与运算 101010100101001001101 时,也就是
1111 & 101010100101001001000 结果:1000 = 8
1111 & 101000101101001001001 结果:1001 = 9
1111 & 101010101101101001010 结果: 1010 = 10
1111 & 101100100111001101100 结果: 1100 = 12

可以看到,当 n 为 2 的幂次方的时候,减一之后就会得到 1111* 的数字,这个数字正好可以掩码。并且得到的结果取决于 hash 值。因为 hash 值是1,那么最终的结果也是1 ,hash 值是0,最终的结果也是0。

HashMap 的容量为什么建议是 2的幂次方?
到这里,我们提了一个关键的问题: HashMap 的容量为什么建议是 2的幂次方?正好可以和上面的话题接上。楼主就是这么设计的。

为什么要 2 的幂次方呢?

我们说,hash 算法的目的是为了让hash值均匀的分布在桶中(数组),那么,如何做到呢?试想一下,如果不使用 2 的幂次方作为数组的长度会怎么样?

假设我们的数组长度是10,还是上面的公式:
1010 & 101010100101001001000 结果:1000 = 8
1010 & 101000101101001001001 结果:1000 = 8
1010 & 101010101101101001010 结果: 1010 = 10
1010 & 101100100111001101100 结果: 1000 = 8

看到结果我们惊呆了,这种散列结果,会导致这些不同的key值全部进入到相同的插槽中,形成链表,性能急剧下降。

所以说,我们一定要保证 & 中的二进制位全为 1,才能最大限度的利用 hash 值,并更好的散列,只有全是1 ,才能有更多的散列结果。如果是 1010,有的散列结果是永远都不会出现的,比如 0111,0101,1111,1110…,只要 & 之前的数有 0, 对应的 1 肯定就不会出现(因为只有都是1才会为1)。大大限制了散列的范围。简单来说,就是容量为2次幂的话,因为在&运算中,2的幂-1都是11111结尾的,所以碰撞几率小。

其中部分为原创,部分内容转载自:https://blog.csdn.net/qq_38182963/article/details/78940047

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值