HashMap-20210602

HashMap 1.7 和 1.8 的一点区别

三个方面:
1、初始化;
2、Hash算法;
3、数据结构;

1、初始化:

  JDK 1.7 版本中,在put动作时,判断是否为空table,是则 inflateTable(threshold) 初始化;
  而 1.8 版本中没有此方法, 在操作put时,直接进行扩容resize();
  -- 此处个人觉得,是一处合并优化;
     初始化和扩容是有相同之处的,都需重新计算符合 "规则" 的capacity、threshold、loadFactor 等;
     因为 "自定义" 的参数不一定符合HashMap的格式即capacity为2 的n 次幂;2^N;

2、Hash算法:

  1.7 版本中,4次位运算+5次异或 来保证高低位都能对 HashValue 有所影响;
  1.8 版本中,则只做了 1次位运算+1次异或 将高16位和低16位,做异或运算;
  --- 从简洁程度,1.8 更胜就是了;但是否有更深入的原因,____________________

3、数据结构:

 1.7 版本中,数组+链表(头插法);1.8  则是,数组 + 链表(尾插法)+ 红黑树;
 --- 修改数据结构,更好的处理冲突、更好的查找;
 --- 提高查询速度;

3.1、树化 & 链化

 参考:  https://blog.csdn.net/weixin_43883685/article/details/109809049
 Hash的分布是遵从泊松分布的;
 此时,当冲突节点在8时,概率已经极其低微了;此时树化,收益最大;

为什么不是 6 或 7 ? 概率也是很低了;

 理想情况下,如若无冲突,那么HashMap的查找复杂度在 O(1); 
 冲突,转链表的话,查找复杂度在O(n); 
 就平均查找复杂度来说,那么链表是 O(n/2) ;
 理解成,遍历n个元素,逐个对比,每个位置都有可能出现,折中就是n/2;(作比较时要相对公平嘛,所以取平均)
 
 对于树化后,则查找复杂度 log(n);

此时计算下;
但深度在 2~ 6 时,发现链表的复杂度和 树化复杂度差距不大;
 
深度         			 2    3       4       5        6        7      8
链表平均复杂度(n/2)       1    1.5     2       2.5      3        3.5    4
树查找复杂度log(n)         1    1.414   1.73    2.236    2.449    2.64   3
 
当子链表的长度为7时,树化才相对有收益,其他情况均和链表查找效率相差不大,均在 0.5 以内;
为了避免频繁在树化-链化之间变换,选择深度在 8 时,进行树化;深度在 6 时,还原链化; 
7 则作为缓冲;似乎比较合适。
好比 加载因子 loadFactor  默认 0.75 也是类似的情况,在时间、空间上的一个斟酌;
取 1 则过满,冲突概率过大; 取0.5 则控件浪费过多;实际 1M 要占用 2M空间;
便折中在一个区域  0.5~0.75; 

3.2、 插入方式&扩容

对于 JDK 1.7 版本,HashMap采用的是头插法,即新元素在表头第一位;
 <事出有因:   最近put的可能最近get, 头部遍历;resize后transfer数据时 也不需遍历链表到尾部再插入; 一切都为了更快>
扩容时,是先扩容再进行插入;
这样一来就会导致扩容后,子链表可能发生倒序情况;且可能在并发时,形成环;
案发现场在resize(int newCapacity) 方法中 transfer () 方法上下三句;
Entry[] newTable = new Entry[newCapacity];
// transfer()方法把原数组中的值放到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 以新覆旧
table = newTable;
----个人理解
简单来说就是,有点脏读的味道;
在线程T1,T2, 假设节点位置,扩容前后恰巧一致;
T1读取子链非最后一个节点后,cpu执行时间用完;待T2 执行完 table = newTable; 后,那么等T1再次执行	transfer();必然形成闭环;因为是先扩容后插入且链表头插法;
参考:https://blog.csdn.net/tianlianye/article/details/87898382

而JDK 1.8 版本,修改了插入方式为尾插法,且发生扩容时,先插入再扩容;
这里有个小点: 扩容时,新旧下标的计算,不同于 1.7中 全部重新计算Hash; 
1.8 则使用了基于长度2^n  的一个小规律,便是,新下标要么与旧下标一致;要么为旧长度+旧下标=新下标;
--个人理解,在实际中,与十进制的算法类似;
换句话将便是,
如果旧hash值,如果小于长度绝对值; 
或者  旧hash值大于长度绝对值,且扩容后依旧大于长度绝对值,那么新旧下标一致;
如:14%16=0余14 、14%32=0余14   37%16=2余5 、37%32=1余5
反之,旧hash值大于长度绝对值,但扩容后小于长度绝对值,那么旧长度+旧下标=新下标(其实就是hash值本身)
如:18%16=1余2 、18%32=0余18
当然这是在10进制的算法;嘿嘿,人家计算下标是2进制的位算法;
所以,规律是这个规律,但是判断的则成了  hashValue 在扩容后 length的 最高位二进制是1 还是 0

扩容定是原来的2倍,在二进制中则是,最高位由0变1;那么此时 (table.length-1) 和 hash 值 做 与运算;
影响扩容前后结果的,当然就是hash值在与长度 (table.length-1) 的最高位相同位置上的,是0 还是1;
如果是1 则 位置变化(旧长度+旧下标=新下标)为 0 不变;
参考:https://blog.csdn.net/qq32933432/article/details/86668385

为什么 hashValue % length = hashValue & ( length - 1 )

参考: https://www.cnblogs.com/ysocean/p/9054804.html

----个人理解:
简单来说,就是用数学方法;将二进制数用十进制运算余数,得出一函数;对其取值范围区间进行约束,从而得出的一个结果;
二进制转十进制:
XnXn-1Xn-2……X1 = Xn2n-1+Xn-12n-2+…+X221+X120
根据除法分配律 左右同除以 2K
得 XnXn-1Xn-2……X1 / 2K = Xn2n-1/2K+Xn-12n-2/2K+…+X221/2K+X120 /2K;
分析:
所谓取余,就是将 除数 被除数 运算后,得到的非整除部分;
好比 3/5=0……3 ;
6/4=1……2;
在k为任意数的前提下,取余算法为 : Xn2n-1/2K+Xn-22n-1/2K+…+X221/2K+X120 /2K;
那么当
①、 0<= k <= n 呢?
可以按照上面所述方式,分子分母约分,如果是整数则去除该项;
最后的非整除部分求和则是余数;

Xn2n-1/2K+Xn-12n-2/2K+…+Xk2k-1/2K+Xk-12k-2/2K+……+X221/2K+X120 /2K;
约分下:
即余数等于 Xk2k-1+Xk-12k-2+…+X221+X120; (此处与原文不同);

②、当 k > n 时,余数即为整个十进制数;
也就是 = Xn2n-1+Xn-12n-2+…+X221+X120
代入k 后,上下一致;
也就是说:
对2的n次幂取余,的代表式为: Xn2n-1+Xn-12n-2+…+X221+X120 ; (n位二进制的十进制算法)

到此为止,结合二进制的移位操作;
可以用右移来代表除法;
那么

一个十进制数对一个2n 的数取余,我们可以将这个十进制转换为二进制数,将这个二进制数右移n位,移掉的这 n 位数即是余数。
 
  知道怎么算余数了,那么我们怎么去获取这移掉的 n 为数呢?

我们再看20,21,22…2n 用二进制表示如下:

0001,0010,0100,1000,10000…

我们把上面的数字减一:

0000,0001,0011,0111,01111…
  根据与运算符&的规律,当位上都是 1 时,结果才是 1,否则为 0。
  
  所以任意一个二进制数对 2n 取余时,我们可以将这个二进制数与(2n-1)进行按位与运算,结果即是余数;
  所以便是 hashValue%length【十进制思考方式】= hashValue & (length-1) 【二进制思考方式】

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值