一文读懂 HashMap 原理

只针对 JDK1.8 版本之后的 HashMap

HashMap 的底层存储结构为数组+链表/红黑树。数组作为 HashMap 的主体,以 Node<K, V>[] 的形式存储,每个节点存储 <K, V> 键值对,支持以 null 值作为 key 或者 value,但也要保证 key 的唯一性,即以 null 作为 key 只能有一个。

HashMap 的默认数组初始化大小为16,扩容时变为原来的 2 倍。并且 HashMap 总是以 2 的幂作为 数组的大小。

hash 值的计算

对于每个 key 来说,都有自己的 hashCode(),以此来计算 HashMap 中的 hash 值。

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

然后通过 (n - 1) & hash 判断当前元素存放的位置。

冲突处理

如果不同元素的位置发生冲突,则采用链地址法,在该位置添加链表,使用尾插法插入,即将新元素添加至链表的尾部。

为什么不使用头插法

在多线程的环境下,使用头插法可能导致链表成环,故牺牲时间采用尾插法,尾插法也会有节点丢失的问题,故多线程情况下应该使用 CurrentHashMap。


若此时链表长度超过了阈值(默认为 8),并且数组的长度大于等于 64,则会将数组转化为红黑树,以提高查询效率。( 链表查找时间复杂度为O(n),红黑树查找的时间复杂度为O(log(n) )

数组扩容

当 HashMap 中的元素越来越多时,由于数组的长度是固定的,Hash 碰撞的几率会越来越高,影响查询效率。

数组扩容时,会遍历所有的元素,根据其 hash 值进行重新的分配,是非常耗时的。

数组扩容每次将数组的大小扩大一倍: newCap = oldCap << 1

数组为什么每次扩容为原大小的二倍?

  1. 容量为2的幂可以使元素均匀的分布在数组中,可以减少 Hash 碰撞。

  2. 数组扩容后,不需要重新计算每个元素的新位置,根据 (n - 1) & hash ,n 扩大一倍,相当于 (n - 1) 的高位多了1,此时再看对应 hash 值的同一位,若该位值为 0,则元素下标不变,若值为 1,(n - 1) & hash多了一位,相当于元素的位置增加了原长度的大小,即元素新下标 = 元素原下标+原数组长度

    举个简单的例子:

    数组长度为 8:

    • (8 - 1) & 3 = 00111 & 00011 = 00011 = 3;
      
    • (8 - 1) & 15 = 00111 & 11111 = 00111 = 7;
      

    数组扩容后长度为 16:

    • (16 - 1) & 3 = 01111 & 001011 = 00011 = 3;
      
    • (16 - 1) & 15 = 11111 & 11111 = 11111 = 15;
      

触发机制

  • 加载因子默认大小为 0.75f,如果超过该阈值:size == (capacity * load factor),则会触发 resize() 进行数组的扩容操作。

  • 如果某一链表长度超过了阈值(默认为 8),而此时的数组长度是小于 64 的,也会触发数组的扩容操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值