Map map = new HashMap<>();
/*
在实例化以后,底层创建了长度16的一维数组Entry[] table
...可能执行了很多次put..
map.put(key1,value1):
首先调用key1所在类的hashCode()计算key1的哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。如果次位置上数据为空,此事key1-value1添加成功。
如果此位置上的数据不为空(意味着此位置上存放着一个或多个数据(以链表形式存放)),比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此事key1-value1添加成功。
如果key1的哈希值和已经存在的某一个数据的哈希值相同,继续比较:调用key1所在类的equal()方法,比较:
如果equal()返回false:此时key1-value1添加成功。
如果equal()返回True:使用value1替换value2.
jdk8相较于jdk7在底层实现方面的不同:
1.new HashMap():底层没有创建一个长度为16的数组
2.jdk8底层数组是:Node[],而非Entry[]
3.首次调用put()方法时,底层创建长度为16的数组
4.底层:数组+链表(jdk 7之前)、数组+链表+红黑树(jdk 8)
当数组的某一个索引位置上的元素以链表形式存在的数据个数大于8且当前数组长度大于64时,此时索引位置上的所有数据改为红黑树存储。
*/
(1)HashMap是以key-value键值对存储的,jdk1.8以后,底层的数据结构是数组+链表+红黑树,数组是HashMap主体,链表是为了解决hash冲突解决的(hash冲突是指两个对象调用hashCode()计算出的哈希值一致导致计算的数组索引值相同),也称“拉链法”解决哈希冲突。当链表长度大于阈值(默认为8)时并且数组的长度大于64时,链表将会转换为红黑树,加入红黑树是为了高效的查询。
注意:如果链表长度大于阈值(默认为8)时并且数组的长度小于64时,链表不会转换为红黑树,而是将数组进行扩容。这样做的目的考虑到数组的长度较小,尽量避开红黑树,这种情况下,转为红黑树,反而会降低效率问题,因为红黑树需要左旋,右旋,变色操作来保持平衡,所以当数组长度小于64,搜索时间会相对快些,考虑到性能和减少搜索的时间,只有当链表长度大于阈值(默认为8)时并且数组的长度大于64时,链表将会转换为红黑树。
(2) HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表形式。
原因:红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
还有选择6和8的原因是:中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
(3) 为什么容量必须是2的n次幂?
我们发现通过数组长度减一在与hash值做按位与运算,能够均匀地减少hash碰撞,2的n次方在二进制中,实际就是1后面有n个零,2的n次方减1在二进制中,实际就是n个1,按位与运算:相同二进制位上,都是1的时候结果为1,否则为0。如果不是2的n次幂,计算出的但索引特别容易相同,产生hash碰撞较大,导致其他索引上的值很大程度上没有发生存储数据,导致链表或者红黑树过长。
(4) 如果我们创建集合时,自己输入的值不是2的n次幂会怎么办?
假如我们输入10,则会将值转换为16,即比这个数大的最小的2的n次幂 通过源码我们能看出:通过无符号右移和按位或运算,最后得出2的n次幂
探讨HashMap在JDK1.8后的底层实现,包括数组+链表+红黑树结构,解决哈希冲突的“拉链法”,及红黑树与链表转换的条件。解释为何容量需为2的幂次方,及非2的幂次方输入的处理。

被折叠的 条评论
为什么被折叠?



