Java HashMap总结

HashMap的数据结构

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

HashMap的hash函数实现原理

一、JDK 1.8 中,经过key的hashCode()方法获得值的高16位异或低16 位实现的:程序员

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

二、使用计算获得的hash值与数组长度n-1作与位运算获得数组下标:数组

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

计算下标时为何用位与运算?安全

  • 因为位运算直接对内存数据进行操做,不须要转成十进制,所以处理速度很是快
  • 正整数对2的倍数取模,只要将数与2的倍数-1作位与运算
  • 对2的倍数取余,只要将数右移2的倍数位

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

  • 计算获得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

扩容过程:spa

  • 建立一个新数组,其容量为旧数组的两倍
  • 将原数组中的元素迁移至新数组。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冲突的解决方法

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

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

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

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

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

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

缺点:须要额外的空间

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

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
相关文章
相关标签/搜索