HashMap的一些总结,有不足的,不对的地方,欢迎讨论,共同进步。
1.1、HashMap简介
HashMap是基于哈希算法的Map接口的实现,是线性不安全的,用于存储键值队,可以允许键值对中的KEY值和Value值为空。JDK1.7HashMap底层是由数组加链表构成的,到了JDK1.8底层由数组+链表+红黑树组成,红黑树的引入是为了解决Hash冲突导致的数据链化严重,导致查询效率不高,链表的时间复杂度为O(n)。
1.2、Hash算法
Hash函数又叫散列算法,其的思想就是在保证输入的消息或数据的唯一性时将其压缩为摘要,以固定长度输出,即将任意长度的输入转化为固定长度的输出,或者hash算法再使用过程中会产生hash冲突,即不同的输入得到的Hash值是相同的,hash冲突是不可避免的,我们只能通过一系列的条件去减少hash冲突的产生。
1.2.1、一个优秀的hash函数具有哪些性质:
①、是一个单向操作、即不能通过输出值逆推出原文
②、Hash碰撞少
③、正向快速、给出原文要在有限的时间有限数的资源计算出其hash值
④、输入敏感、原始数据即使只有细微的修改其hash只也大有不同
1.2.2、hashmap中的hash应用
Hashmap的hash方法是通过二次加工得到的,通过hashcode异或自己的高16位得到的,这样处理就是为了将高16位的影响带入到低16位,因为,在HashMap中并不是直接用hashcode作为数组的索引,如下图所示,最终数组的索引是hash值&数组的长度减1,即真正有作用的就hash值的后几位(具体几位由当前数组的长度决定);本来hashcode的散列度是十分高的,但是只取后几位就会大大增加hash冲突,例如有两个hashcode EEEEEEEE33与FFFFFFFF33,在只取后8个字节是他们的hash值是相同的,经过hashmap将高位于低位&,这样就得到了22和33就不再产生冲突了。
Hashmap有三种构造方法,无参构造、单参构造和双参构造,这里只讲一下双参数的构造方法,第一个参数为数组初始容量,默认为16,第二个为负载因子,默认为0.75,调用构造方法时并不会新建数组,数组的建立是在putval方法里面。
数组初始容量有一个自修订的一个函数,用来修正数组容量不为2的n次方,如下代码的思想就是将输入值中嵌套的0全部变为1,具体的得自己一步一步试一下才能体会到这个思路的精妙。
1.3.1、为什么规定数组的size要是2的N次方?
首先我们得从位运算的角度取考虑这个问题,以16来举例:16的二进制形式为10000,比16小的二进制的可能有16种,为(0000,1111),这代表囊括了hashcode的后四个bit的所有可能,这样就会减少hash碰撞的几率,举个例子,假设我分别设定数组的size一个为16,一个为10,现给出两个hashcode 15 和 9,则通过计算size为16的数组的索引分别是15和9,而size为10的数组索引则为 9、9,产生了hash碰撞。
1.4、Hashmap的结构
1.4.1、hashmap的Node结点
以键值对的形式保存,只有一个next指针,指向后面插入的数据
1.4.2、treenode
包含了父节点,左孩子右孩子以即原先node的链表关系,即在红黑树的内部保存了链式关系,只是查询的时候不会用到,一般用在删除,插入。
1.2、Hashmap的put方法
hashmap的put方法里面会建立数组,数组的size由构造方法决定,默认为16和0.75;
数组的索引也是在put方法计算得来的,即hash值&size-1,此时插入操作有四种情况:
第一:当数组对应的桶位为null时,此时该桶位还没有被占用,直接放就好
第二:当数组对应的桶位不为null,如果此时的key值相等,直接就是一个replace操作;
第三:当数组对应桶位不为null,并且此时当前key值不相等,则采用后插法构成链表形式
第四:如果此时已经构成了链表,并且当此时链表长度大于等于8的时候,会判断此时数组的size是不是大于等于64,如果是则,链表转换为红黑树,如果否,就扩容数组不进行树化操作。
1.5.1为什么设定链表长度大于8的时候再进行树化?
JDK1.8引入红黑树就是嫌链表查询速度慢,就是想节省时间,但是将链表进行树化需要一定的时间。红黑树的一个结点占用的资源是链表的两倍,Java权衡了这两者之间的时间空间的关系。并且开发者发现,在负载因子为0.75时,要想达到链表长度为8的几率其实特别小,不容易树化。如下文所示。
1.5.2 Hashmap的扩容机制
当链表长度大于8并且数组长度小于64时,以及当插入的数量大于阈值时会触发扩容机制,将原先的数组长度增大两倍,此时也有几种情况:
第一种:当老数组桶位中没有数据时,即为null时,此时新数组对应桶位也为null;
第二种:当此时老数组桶位中的数据没有树化也没有链化时,此时即会重新计算哈希表的索引
其中e.hash保存的是调用hashcode得到的值而不是老数组的哈希索引值,此时数组的长度增加了两倍,所以得重新计算。
第三种:当此时桶位得数组已经链化和树化时,此时扩容方式比较特殊,以链表来讲就是通过将链表拆分为两个链表,高位链表和低位链表。
它判断是hashcode值的第log (oldcap)位的状态,是0就放到低位链表中,1则放到高位链表中。判断完全之后,低位表的索引直接时继承老数组的索引位置,即原先在老表是4新表的索引也是4。高位表的下标则是老表下标加上老表表长,即老表表长位16,索引为4在新表的索引就是4+16 =22;
红黑树的扩容是和链表一样,树节点仍然保留了next字段,我们在查询的时候next字段不起作用,但是在增加以及删除时next字段就会起作用,此时也是通过高低位去拆分为为高低位链,在将链表进行判断,如果此时表长小于等于6时,红黑树就会退化为一个新的链表。
1.3、红黑树简介
红黑树比较复杂,这里只简单提一下,(write by 飘啊飘飘啊飘)红黑树是一颗自平衡的二叉查找树,前人在树的基础上提出了平衡树的概念,但是又发觉一颗平衡树的维护成本比较高,条件比较苛刻,在有些时候并没有达到人们预想的效果,因此诞生了红黑树。推荐大家一个视频,讲的非常好,作者讲解了从2-3-4树演化红黑树的思路:
https://www.bilibili.com/video/BV135411h7wJ?from=search&seid=16324446913516753135
1.6.1、红黑树的性质:
①、由红色结点与黑色结点构成
②、根节点为黑色
③、所有的叶节点为黑色
④、每个红色结点必须有2个黑色子节点
⑤、从叶子结点到根结点路径上含有的黑色结点数目相同