简介

HashMap是平常使用的非常多的,内部结构是 数组+链表/红黑树 构成,很多时候都是多种数据结构组合。

我们先看一下HashMap的基本操作:

 

new HashMap(n);

第一个知识点,传入n,构造的HashMap容量就是n吗?

答案是:不一定。

    public HashMap(int initialCapacity, float loadFactor) {
this.loadFactor = loadFactor; //负载因子 默认0.75
//设置容量
this.threshold = tableSizeFor(initialCapacity);
}

  

tableSizeFor 这段代码其实就做了一件事,例如,你初始化给了10,它会给你16,大于10的是2的k次幂。

以初始值50为例,讲一下实现原理:

  static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

  

算法就是让二进制不断右移,与自己异或,把第一位为1(最高位)后面全变为1,111111 + 1 = 1000000 = 26 2^62
6
(符合大于50并且是2的整数次幂 )

 

第二个知识点,回答开题的问题,为什么hash函数这么设计?

HashMap的hash函数是根据Key值计算的;
一定要尽可能降低hash碰撞,越分散越好;
算法一定要尽可能高效,因为这是高频操作;
再来看一下这段代码:

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

  

这段代码有个名字,叫扰动函数,大家想一下,如果hash函数直接使用key.hashCode()作为hash 值怎么样?

key.hashCode()获得的是key的hashcode(), 如果HashMap数组长度为16,求对象在数组存储位置 (n - 1) & hash 就相当于 0000 1111 & hash ,让 hash 高位全部置为0,只用到了 hash 的低位,因为只用了低位,碰撞的几率就会比较大。

聪明的算法设计者兼顾性能和降低碰撞,就考虑用高16位和低16位结合起来异或形成hash 值。如下图所示,

 

第三个知识点,相比1.7,JDK1.8做了哪些优化?

1.7 使用头插法,1.8使用尾插法;
1.7 hash函数使用4次位运算+5次异或,1.8使用1次位运算+1次异或;
1.7 使用数组+链表的结构,1.8 使用数组 + 链表 +红黑树;
1.7 扩容需要对原始元素重新hash & (len -1), 1.8 计算元素新位置 = 原始位置 / 原始位置 + 旧容量;
下面开始解释说的四条:

第一条:
1.8 之前都是使用头插法,因为作者认为现在插入的数据是热乎的,最有可能被立即使用到,所以用头插法;

而为什么1.8用尾插法呢,如果是头插法,在多线程环境下,会出现这样一个问题:A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环,如下图所示:

 

第二条:
1.7的hash 函数如下,可以和上面的对比看:

static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

  

四次无符号右移 五次异或

第三条:
画了一张插入流程图如下:
注意4个点:

先插入新节点再扩容(1.7是先判断容量,不够先扩容再插入);
先判断是否为红黑树,链表插入结束判断是否是否应该转为红黑树;
红黑树转为链表的临界值是6不是8,原因是如果长度经常在8附近,转来转去,浪费资源。
为什么红黑树的阈值是8,因为合理的hash函数,发生碰撞链表长度为8的概率作者计算为千万分之后。

 
   // 作者给的hash冲突链表长度分别为以下值得概率
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006

  

第四条:
1.7 扩容后会采用 hash & (len -1)重新计算所有数组元素的位置,但是1.8采用简单快捷的方式定位新位置: 直接放在原位置/ 原位置 + 旧容量

这个怎么理解呢?看下面这张图,

 

分二种情况:

比如现在 数组长度为16,元素的hash值为0101 , 0000 0101 & 0000 1111 = 0000 0101,
扩容之后,因为高位为0,0000 0101 & 0001 1111 = 0000 0101,位置没变,可以直接放到扩容后的原始位置。
数组长度为16,原始的hash值为 0001 0101, 0001 0101 & 0000 1111 = 0101, 扩容到32之后, 0001 0101 & 0001 1111 = 0001 0101, 比原来的位置大16。
有意思吧! 好好品,越品越有意思!
截取了一段扩容代码

final Node<K,V>[] resize(){
//***
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}

  

 

最新文章

  1. Visual Studio 2013 Unit Test Project App.config文件设置方法
  2. 设置bundle包中的默认语言
  3. Angular从0到1:function(上)
  4. 利用servlet3.0上传,纯原生上传,不依赖任何第三方包
  5. 夺命雷公狗---node.js---3commonJs 与 nodeJs的简介
  6. Struts2 DMI的使用
  7. MySQL(12):windows下解决mysql忘记密码
  8. zf-关于荆州图片链接和弹出页面问题
  9. Jmeter Boss系统login
  10. php生成雪花图像(不美观请见谅)
  11. 淘宝NPM源的使用
  12. 【面试】吃透了这些Redis知识点,面试官一定觉得你很NB(干货 | 建议珍藏)
  13. 微信小程序学习 一
  14. es6 语法 (类与对象)
  15. Debian 8.9 搭建wordpress个人博客
  16. 亿级 ELK 日志平台构建部署实践
  17. Ubuntu16.04安装xgboost
  18. IE初始
  19. 2018年省赛蓝桥杯JavaB组
  20. 通过SVN获取变更列表,得到对应的最新class

热门文章

  1. poj1050 To the Max(降维dp)
  2. stm32与红外遥控器(NEC协议)
  3. Disconnected from the target VM, address: &#39;127.0.0.1:1135&#39;, transport: &#39;socket&#39;-SpringBoot启动报错
  4. B树摘要
  5. vue 路由知识点(一级路由与二级路由嵌套)
  6. A. Peter and Snow Blower 解析(思維、幾何)
  7. Rancher 2.5特性解读丨更简单友好的API和Dashboard
  8. LoRa联盟的简介
  9. python数据类型之dict(字典)
  10. eyou升级弹窗、云插件库、接口配置、功能开关【按需显示插件】