HashMap的数据结构

底层由数组和链表组成,数组是主体,链表是为了解决Hash冲突,jdk1.8后,当链表的长度大于阈值8时,会转换为红黑树

HashMap的hash函数实现原理

1、JDK 1.8 中,通过key的hashCode()方法得到值的高16位异或低16 位实现的:

(h = k.hashCode()) ^ (h >>> 16)

2、使用计算得到的hash值与数组长度n-1做与位运算得到数组下标:

if ((p = tab[i = (n - 1) & hash]) == null)

计算下标时为什么用位与运算?

  • 由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快
  • 正整数对2的倍数取模,只要将数与2的倍数-1做位与运算
  • 对2的倍数取余,只要将数右移2的倍数位

为什么使用异或运算?
这段代码叫“扰动函数”,目的是为了混合原hash码的高位和地位,混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相的保留下来

  • 计算得到hash值后需要与数组长度-1做与位运算得到元素所在数组下标位置,此时数组长度-1相当于一个“低位掩码”,结果是hash值的高位全部归零,只保留低位值
  • hashCode函数返回的int值范围为32位,右移16位再异或,相当于自己的高半区和低半区做异或,这样得到的hash值混合了原始hash码高位和低位的特征,减少了hash碰撞的几率

为什么用异或,不用与和或运算?
image
图片转自:https://zhuanlan.zhihu.com/p/...

HashMap的put方法执行过程

  • 调用哈希函数获取Key对应的hash值,再计算其数组下标;
  • 如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;
  • 如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;
  • 如果结点的key不存在,直接插入,如果已经存在,则替换其value;
  • 如果集合中的键值对大于12,调用resize方法进行数组扩容。

HashMap的容量与扩容机制

初始容量与负载因子:

  • HashMap默认初始容量为16,负载因子为0.75

    • 负载因子为0.75是在容量浪费和hash冲突增多之间取的一个值
  • 指定初始容量时推荐使用2的倍数作数组长度

    • 因为只有2的倍数在减1的时候,才会出现01111这样的值,才能用来做与位运算替代取模运算
    • 如果指定的初始容量不为2的倍数,则会寻找比原始值大的最小的那个2的倍数值,如传17,则初始容量为32

扩容过程:

  • 创建一个新数组,其容量为旧数组的两倍
  • 将原数组中的元素迁移至新数组。jdk1.8以下需要重新计算下标位置,jdk1.8后做了优化,只需计算hash & oldCap的值即可确定元素在新数组中的位置(oldCap为原数组长度),分以下两张情况:

A:扩容后,若hash值新增参与运算的位=0,那么元素在扩容后的位置=原始位置
B:扩容后,若hash值新增参与运算的位=1,那么元素在扩容后的位置=原始位置+原数组长度。

image

所以说扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。

为什么使用需要链表转红黑树

  • 引入红黑树就是为了查找数据快,解决链表查询深度的问题。链表查询效率为O(n),当长度过长时查询效率慢,红黑树查询效率O(lgn),查询效率快
  • 红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持树的平衡
  • 构建红黑树和维护红黑树的平衡也需要一些操作损耗资源,所以在链表长度大于8时才转换成红黑树

红黑树:

  • 每个节点非红即黑
  • 根节点总是黑色的
  • 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
  • 每个叶子节点都是黑色的空节点(NIL节点)
  • 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

jdk8中对HashMap做了哪些改变?

  • 在java 1.8中,如果链表的长度超过了8,那么链表将转换为红黑树。
  • 发生hash碰撞时,java 1.7 会在链表的头部插入,而java 1.8会在链表的尾部插入
  • 扩容迁移数据时,jdk1.8没有重新计算新数组下标,而是通过hash & 原数组长度来确定元素在新数组中的下标
  • 在java 1.8中,Entry被Node替代(换了一个马甲)。

hash冲突的解决方法

1、开放地址法
当出现hash冲突时,执行左右空节点探测

  • 线性探测再散列,当hash值p出现冲突时,则将数据放到 (p + 1) % m处 ,逐步向后探测
  • 二次探测再散列,当hash值p出现冲突时,则将数据放到 (p + 1) % m 处,如果此时还存在冲突,则将数据放到 (p - 1) % m 处,左右探测
  • 伪随机探测再散列,构建一个随机数列探测,di依次取数列中的值

缺点:不能删除节点、只能对节点作删除标记;要求哈希表空间大于或等于装填数据数目。

2、再hash法
构造多个hash函数,当一个冲突时,再计算第二个的hash值,直到不冲突为止

优缺点:这种方式不容易产生聚集,但增加了计算时间

3、链地址法
出现冲突时用链表链接

缺点:需要额外的空间

4、建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表中元素发生冲突的元素均存入溢出表

ConcurrentHashMap数据结构

jdk1.7:

  • 使用一个Segment数组和多个HashEntry数组组成
  • 当执行put操作时,会进行第一次key的hash来定位Segment数组的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到对应的HashEntry的位置,再通过ReentrantLock进行加锁后,将数据添加到链表尾部

jdk1.8:

  • 与HashMap结构一致,底层是数组+链表或数组加红黑树的结构实现,当链表的节点数大于8时会转换为红黑树
  • 操作流程是,对key进行hash运算定位到数组下标,如果下标位置table为空则先初始化,再cas插入,如果有数据,则用同步锁Synchronized进行加锁后插入

jdk1.7数据结构图:
image

HashMap,LinkedHashMap,TreeMap 有什么区别?

  • HashMap 通过hash函数计算数组下标,key是无序的
  • LinkedHashMap通过双向链表保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;
  • TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也~~~~可以指定排序的比较器)

HashMap、TreeMap、LinkedHashMap的使用场景

  • HashMap:一般情况下,使用最多的是 HashMap。只需要普通的在 Map 中插入、删除和定位元素时;
  • TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;
  • LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。

HashMap 和 HashTable 有什么区别?

  • HashMap 是线程不安全的,HashTable 是线程安全的;
  • HashMap允许插入null键,HashTable不允许;
  • HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
  • HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

东瓜
18 声望3 粉丝

« 上一篇
HashMap面试题
下一篇 »
Mysql总结