jdk7 HashMap实现原理
数组加链表。先确定数组下标,下标相同再用链表连接。
创建
在HashMap<> hash = new hashMap()之后,如果使用无参构造器创建,数组默认大小是16,也就是2的四次方。扩容阈值默认是0.75。当数组容量达到 数组长度乘以阈值 , 数组扩容为原来的两倍。
put方法的实现过程
调用put(key,value)方法, 源码进行hash(key)方法,获得一个int k = key.hashCode()。
再根据 k & (hash.length - 1) 得到一个int数字,也就是数组的下标(事实上没有这么简单,k要经过一系列的 ^ , >> , << 等运算来进行,原因在下文《哈希碰撞》说明)。
&运算:同位都为1结果为1,否则为0。而且,hash的底层数组长在扩容时,长度都是2的次方。例如,1000 0000,或者0010 0000,这样的意义在于,长度减一之后都是 0111 1111,0001 1111这样的二进制数,再进行与运算得到的结果,一定小于这个二进制数,也就是一定不会数组下标越界。
创建一个Entry对象,里面有四个值,如图。把对象存入hashMap。
size++。 (hashMap的长度,也就是所有数据的个数)
put是有返回值的。
运行结果,返回被覆盖掉的value。
put扩容:根据新生成的数组长度重新计算存放位置。
(在jdk4之前采用取余操作而不是&,但是太慢了,后来改进了。)
哈希碰撞
实际在确定下标的过程中,很可能出现数组下标被占用的情况,此时下标相同的元素就要用链表来实现存储。后来的元素插入到链表的头部。具体方法自行查看链表的内容。
上文提到,key经过hashCode()方法得到的值并不是直接与 hash.length()-1 进行&运算,而是经过一系列的异或,位移运算再进行&,为了让k的高位也能参与运算。这样,就能尽可能平均的分布数据,避免链表过长。
null值存放
规定存放在0下标位置。
get方法
通过key去计算hashCode,找到数组下标,遍历链表,找到key相等的,返回value。
jdk8 HashMap实现逻辑
与jdk7的区别:
1.引入红黑树
在链表长度>=8时把链表结构转换为红黑树结构。 <=6时转换回链表结构。 之所以不完全根据8来转换,是避免频繁的变动来加大开销。
注意!并不是链表大于等于8的时候一定会转换红黑树,因为如果数组长度小于64,不会进行树化,而是扩容。因为对于长久眼光来说,此时数据尚少,后续很可能有更多数据传入,总会扩容,而且根据之前提到的扩容方式,又会重新匹配下标,那么此时的树化操作很可能是无用功。总之,树化操作只有在数组长度达到64的时候进行,因为此时如果再通过数组扩充来优化,空间开销就太大了。
2.简化了在计算下标时候,哈希值的异或和位移操作。
先要明白,jdk7中之所以要进行这么多的操作,是为了尽可能的平均分配数据,避免大量下标相同,导致链表过长。而链表的查询操作是很慢的。
那jdk8中,引入了红黑树,就是不怕链表过长。大不了用红黑树储存。因此,过多的异或位移操作反而加大了开销。所以简化。
3.在链表尾部添加。
为什么jdk7中是在链表头部添加而到了jdk就变为了尾部添加呢?
对链表来说,添加操作的开销主要在于根据nextNode一个一个的查找,而在头部添加,只需要把先添加的对象的next改成原来头部的地址就好了。那么为什么jdk1.8反其道行之呢?
还是因为红黑树的关系,判断是否对链表进行树化的依据是 数组是否达到预期长度(默认64), 链表长度是否>=8, 因此,需要对链表进行遍历,而遍历的过程,自然而然就拿到了最后一个元素,此时,在尾部添加新对象反而省力。
--------------------------如有问题,欢迎指正。