HashMap源码

JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列。 HashMap 通过 key 的hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置,如果定位到的数组位置不含链表,就可以直接插入,时间复杂度为O(1),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。链表出现的越少,HashMap性能越高。

相⽐于之前的版本, JDK1.8 之后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为8)(将链表转换成红⿊树前会进行一个判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。

1.7和1.8的区别
  • HashMap在1.7中是由 数组+链表 也可以说 哈希表+链表 实现的,

    HashMap在1.8中是由 数组+链表 +红黑树

  • JDK1.7中,HashMap存储的是Entry对象

    JDK1.8当中,HashMap存储的是实现Entry接口Node对象

  • JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法

    • 为什么从头插法改成尾插法?

      在1.7中,是没有红黑树的,在并发的情况下单链表过长会成环发生死循环

      在1.8中,尾插法就可以解决这个问题

    • 1.8也没有解决数据覆盖的问题

  • 扩容机制

    • JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,如果这次插入的并没有发生Hash冲突的话,那么就会造成一次无效扩容

      但是在1.8的时候是先插入再扩容的,优点是可以减少1.7的一次无效的扩容,因为如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容

    • 扩容过程不一样(上面)

HashMap中获取hash值的方法

首先判断 key 是否为 null,为 null 则返回 0 ,所以 key 为空的元素对应的数组坐标一定是 0,而且根据 put 会覆盖相同 key 的逻辑来思考,key 为空的元素最多只有一个。不为 null 则返回 key 的 hashCode异或上它的高16位。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap扩容
进行位运算的原因

(h >>> 16)是无符号右移16位的运算,右边补0,得到 hashCode 的高16位 (h = key.hashCode()) ^ (h >>> 16) 把 hashCode 和它的高16位进行异或运算,可以使得到的 hash 值更加散列,极可能减少哈希冲突,提升性能。 而这么来看 hashCode 被散列 (异或) 的是低16位。原因是获取 key 所对应数组下标的方式是 hash值 % 数组长度。比如 10001 % 101 取余的结果只会看前面二进制数的低三位,其余高位不影响取余结果。而 HashMap 数组长度一般不会超过2的16次幂,那么高16位在大多数情况是用不到的,所以只需要拿 key 的 HashCode 和它的高16位做异或即可让hash值更加散列。

为什么链表出现的越少,HashMap性能越高

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

put的流程(1.8)

1.通过hash函数计算key的hash值,调用putVal方法

2.如果hash表为空,调用resize()方法创建一个hash表

3.根据hash值索引hash表对应桶位置,判断该位置是否有hash碰撞

  3.1 没有碰撞,直接插入映射入hash表

  3.2 有碰撞,遍历桶中节点

    3.2.1 第一个节点匹配,记录该节点

    3.2.2 第一个节点没有匹配,桶中结构为红黑树结构,按照红黑树结构添加数据,记录返回值

    3.2.3 第一个节点没有匹配,桶中结构是链表结构。遍历链表,找到key映射节点,记录,退出循环。       没有则在链表尾部添加节点。插入后判断链表长度是否大于转换为红黑树要求,符合则转为红黑树结构

    3.2.4 用于记录的值判断是否为null,不为空则是需要插入的映射key在hash表中原来有,替换值,返回旧值putValue方法结束

总结:HashMap在put方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。如果索引处为空,则直接插入到对应的数组中,否则,判断是否是红黑树,若是,则红黑树插入,否则遍历链表,若长度不小于8,则将链表转为红黑树,转成功之后 再插入。

为什么HashMap的容量大小要取2的指数倍?

有两个原因:1提升计算效率:因为2的指数倍的二进制都是只有一个1,而2的指数倍-1的二进制就都是左全0右全1。那么跟(2^n - 1)做按位与运算的话得到的值就一定在【0,(2^n - 1)】区间内这样的数就刚合适可以用来作为哈希表的容量大小,因为往哈希表里插入数据,就是要对其容量大小取余,从而得到下标。所以用2^n做为容量大小的话,就可以用按位与操作替代取余操作,提升计算效率2.便于动态扩容后的重新计算哈希位置时能均匀分布元素:因为动态扩容仍然是按照2的指数倍,所以按位与操作的值的变化就是二进制高位+1,比如16扩容到32,二进制变化就是从0000 1111(即15)到0001 1111(即31)那么这种变化就会使得需要扩容的元素的哈希值重新按位与操作之后所得的下标值要么不变,要么+16(即挪动扩容后容量的一半的位置),这样就能使得原本在同一个链表上的元素均匀(相隔扩容后的容量的一半)分布到新的哈希表中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值