HashMap底层源码实现

本文深入探讨了HashMap的数据结构,包括数组与链表的结合,以及在JDK1.8中引入的红黑树优化。详细解释了键值对的存储与查找机制,以及在不同条件下HashMap如何进行扩容和重新计算元素位置。
摘要由CSDN通过智能技术生成

首先需要明确的是

HashMap 内部结构:可以看作是数组和链表结合组成的复合结构,数组被分为一个个桶(bucket),每个桶存储有一个或多个Entry对象,每个Entry对象包含三部分:key(键)、value(值),next(指向下一个Entry),通过哈希值决定了Entry对象在这个数组的寻址;哈希值相同的Entry对象(键值对),则以链表形式存储。如果链表大小超过树形转换的阈值(TREEIFY_THRESHOLD= 8),链表就会被改造为树形(红黑树)结构。

问:那HashMap在存储键值对时,是如何确定存储的位置的?

答:通过hashcode()方法计算键的哈希值==>得到的是一个整数。

int hash = key.hashcode()

       JDK1.8中采用的是位运算。在特定情况下,位运算可以转换成取模运算(当 b = 2^n 时,a % b = a & (b - 1) )。也是因此,HashMap 才将初始长度设置为 16,且扩容只能是以 2 的倍数(2^n)扩容。源码中使用位运算hash & (length - 1) 代替取模运算hash%length,是利用其可以直接对内存数据进行操作,不需要转换成十进制,来提高运算效率。

static int indexFor(int h, int length) {
 
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
 
return h & (length-1);
 
}

问:若得到的数组下标相同怎么办?会如何插入?

答:如果该位置已有元素存储,会插入在链表的头部。

// 将新进元素指向链表的头部,再将头节点指向新元素
table[i] = new Entry(key,value,table[i]);

 这里写图片描述

问:什么时候进行扩容?

答:当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

问:怎么进行扩容?(JDK1.8)

答: 图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

 

问:为什么hashMap的容量扩容时一定是2的幂次方?

答:首先需要知道什么是与运算,都为1,才能得到1.我们在计算得出hash值后,是如何计算存储的位置的。利用的与运算,参考上面。

若HashMap容量为15,length-1的二进制位1110

那么两个索引的位置都是14,就会造成分布不均匀了,

增加了碰撞的几率,

减慢了查询的效率,

造成空间的浪费。 

HashMap在1.7和1.8做的比较大的一个改变

    • JDK1.7是数组+链表,用的头插法——导致链表成环——如何成环的分析一下。

    • JDK1.8是数组+链表+红黑树,用尾插法来避免逆序。

    • JDK1.7直接用hash值和需要扩容的二进制数进行&与操作

    • JDK1.8沿用了JDK1.7的计算规律,也就是扩容前的位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7那种异或的操作,只需要判断hash值的新增参与运算的位是0还是1就可以了

扩容后JDK1.7

1.7之前使用的是数组加链表,它的数据节点是一个Entry节点,就是它的一个内部类,1.7之前数据插入的过程是使用头插法,但是HashMap使用头插法,它在扩容的一个过程,里面有一个resize方法,他又调用了一个transfer的方法,把里面的一些entry进行了一个rehash,在这个过程当中,可能会造成一个链表的环,就可能在下一次get的时候出现一个死循环的情况,其次它也没有加锁,在多线程并发的情况下,不能保证数据的安全性。

在多个线程并发扩容时,会在执行transfer()方法转移键值对时,造成链表成环,导致程序在执行get操作时形成死循环

 

JDK1.8进行了改变,改为数组+链表+红黑树,把原来的一个Entry节点变成了一个Node节点,它整个put过程也做了一个优化。

参考

https://blog.csdn.net/u012156116/article/details/81206649

https://blog.csdn.net/u010890358/article/details/80496144

https://www.cnblogs.com/LiaHon/p/11149644.html

https://blog.csdn.net/gududedabai/article/details/85784161

https://www.cnblogs.com/williamjie/p/9358291.html

内容概要:该题库专为研究生入学考试计算机组成原理科目设计,涵盖名校考研真题、经典教材课后习题、章节题库和模拟试题四大核心模块。名校考研真题精选多所知名高校的计算机组成原理科目及计算机联考真题,并提供详尽解析,帮助考生把握考研命题趋势与难度。经典教材课后习题包括白中英《计算机组成原理》(第5版)和唐朔飞《计算机组成原理》(第2版)的全部课后习题解答,这两部教材被众多名校列为考研指定参考书目。章节题库精选代表性考题,注重基础知识与重难点内容,帮助考生全面掌握考试大纲要求的知识点。模拟试题依据历年考研真题命题规律和热门考点,精心编制两套全真模拟试题,并附标准答案,帮助考生检验学习成果,评估应试能力。 适用人群:计划参加研究生入学考试并报考计算机组成原理科目的考生,尤其是需要系统复习和强化训练的学生。 使用场景及目标:①通过研读名校考研真题,考生可以准确把握考研命题趋势与难度,有效评估复习成效;②通过经典教材课后习题的练习,考生可以巩固基础知识,掌握解题技巧;③通过章节题库的系统练习,考生可以全面掌握考试大纲要求的各个知识点,为备考打下坚实基础;④通过模拟试题的测试,考生可以检验学习成果,评估应试能力,为正式考试做好充分准备。 其他说明:该题库不仅提供详细的题目解析,还涵盖了计算机组成原理的各个方面,包括计算机系统概述、数据表示与运算、存储器分层、指令系统、中央处理器、总线系统和输入输出系统等。考生在使用过程中应结合理论学习与实践操作,注重理解与应用,以提高应试能力和专业知识水平。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值