HashMap底层源码解析上(超详细图解+面试题),dubbo工作原理面试题

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

面试题


面试题1:哈希表底层采用何种算法计算hash值?还有哪些算法可以计算出hash值?

  • 底层采用的是key的hashCode方法的值结合数组长度进行无符号右移(>>>),按位异或(^),按位与(&)计算出索引。还可以采用平方取中法,取余数,伪随机数法

面试题2:当两个对象的hashCode相等时会怎么样?

  • 会产生hash碰撞,若key值内容相等则替换旧的value,反之则连接到链表后面,链表长度超过阈值8就转换成红黑树存储

面试题3:何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞

  • 只要两个元素的key计算的hash码值相同就会发生hash碰撞,jdk8前使用链表解决哈希碰撞,jdk8之后使用链表+红黑树解决哈希碰撞

面试题4:如果两个键的hashcode相同,如何存储键值对

  • hashcode相同则会调用equals比较内容是否相同,如果相同则会用新value值覆盖老value值,如果不相同则会连接到链表后面

HashMap集合的继承关系

============================================================================

在这里插入图片描述

在这里插入图片描述

从图中可以看出HashMap实现了Map,Cloneable,Serializable接口

  • Cloneable空接口,表示可以克隆,创建并返回HashMap对象的一个副本。

  • Serializable序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化

  • AbstracMap父类提供了Map实现接口,以最大限度的减少实现此接口所需的工作

知识补充:

通过上述继承关系我们发现一个很奇怪的现象,HashMap已经继承了AbstractMap而AbstractMap已经实现了Map接口,那为什么HashMap还要再实现Map接口呢?同样在ArrayList中LinkedList中都是这种结构

据java集合框架的创始人Josh Bloch描述,这样的写法是一个失误,在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识错了。显然的,JDK的维护者后来不认为这个小小的失误值得去修改,所以就这样存在下来了

代码如下(示例):

HashMap集合类的成员

===========================================================================

1.序列化版本号

private static final long serialVersionUID = 362498820763181265L;

2.集合的初始化容量(必须是2的n次幂)

//默认的初始容量是16 – 1<<4相当于12的4次方—116

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

问题:为什么必须是2的n次幂?如果输入值不是2的n次幂比如10会怎样

HashMap的构造方法可以指定集合的初始化容量大小:

public HashMap(int initialCapacity) {

this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

根据上述讲解我们已经知道,当向HashMap中添加一个元素的时候,需要根据key的hash值去确定其在数组中的具体位置。HashMap为了存取高效,要尽量减少碰撞,把数据均匀分配,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法中

这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算(这点上述已经讲解)。所以源码中做了优化,使用hash&(length-1),而实际上hash%length等于hash&(length-1)的前提是length是2的n次幂

为什么使用hash&(length-1)能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n-1次方实际就是n个1

举例:

说明:按位与运算:相同的二进制数位上,都是1的时候,结果为1,否则为0。

在这里插入图片描述

在这里插入图片描述

如果我们设置的数组大小不是2的幂会怎么样

public HashMap(int initialCapacity) {

this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

我们以设置initialCapacity为10为例,他会在itableSizeFor方法中将非2次幂的数转换成离他最近且比它大的二次幂数

static final int tableSizeFor(int cap) {

int n = cap - 1;

n |= n >>> 1;

n |= n >>> 2;

n |= n >>> 4;

n |= n >>> 8;

n |= n >>> 16;

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

下面分析这个算法

1.首先,为什么要对cap做减1操作。int n = cap - 1;

这是为了防止cap已经是2的幂,如果cap已经是2的幂,没有减1操作的话,方法最后返回的capacity将是这个cap的2倍

2.如果这时n为0,经过cap-1之后,则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作),我们这里只讨论n不等于0的情况

3.|(按位或运算):运算规则,相同的二进制数位上,都是0的时候,结果为0,否则为1。

  • 第一次右移n |= n >>> 1;

  • 在这里插入图片描述

  • 第二次右移n |= n >>> 2;

  • 在这里插入图片描述

  • 第三次右移n |= n >>> 4;

  • 在这里插入图片描述

  • 以此类推,无论我们再右移,也只是与0做|运算,结果不变

  • 请看下面一个完整的例子

  • 在这里插入图片描述

所以执行完tableSizeFor方法后,我们所传入的非2次幂cap会被转化成大于等于它的一个离他最近的2次幂数

3.默认的负载因子,默认值是0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f;

  • 我们的扩容阈值为cap*DEFAULT_LOAD_FACTOR,当容量为16时,他会在容量达到12时便触发扩容

4.集合的最大容量

//集合最大容量的上限是:2的30次幂

static final int MAXIMUM_CAPACITY = 1 << 30;

5.链表的值超过8则会转成红黑树

//当桶(bucket)上的节点数大于这个值时会转成红黑树

static final int TREEIFY_THRESHOLD = 8;

问题:为什么Map桶中节点个数超过8才转为红黑树

======================================================================================

8这个阈值定义在HashMap中,针对这个成员变量,在源码的注释中只说明了8是bin(bin就是nucket桶)从链表转成树的阈值,但是并没有说明为什么是8

在HashMap中有一段注释说明:我们继续往下看:

  • Because TreeNodes are about twice the size of regular nodes, we

  • use them only when bins contain enough nodes to warrant use

  • (see TREEIFY_THRESHOLD). And when they become too small (due to

  • removal or resizing) they are converted back to plain bins. In

  • usages with well-distributed user hashCodes, tree bins are

  • rarely used. Ideally, under random hashCodes, the frequency of

  • nodes in bins follows a Poisson distribution

  • (http://en.wikipedia.org/wiki/Poisson_distribution) with a

  • parameter of about 0.5 on average for the default resizing

  • threshold of 0.75, although with a large variance because of

  • resizing granularity. Ignoring variance, the expected

  • occurrences of list size k are (exp(-0.5) * pow(0.5, k) /

  • factorial(k)).

翻译:

因为树节点的大小大约是普通节点的两倍,所以我们只在箱子包含足够的节点时才使用树节点(参见TREEIFY_THRESHOLD)。

当他们边的太小(由于删除或调整大小)时,就会被转换回普通的桶,在使用分布良好的hashcode时,很少使用树箱。

理想情况下,在随机哈希码下,箱子中节点的频率服从泊松分布

第一个值是:

  • 0: 0.60653066

  • 1: 0.30326533

  • 2: 0.07581633

  • 3: 0.01263606

  • 4: 0.00157952

  • 5: 0.00015795

  • 6: 0.00001316

  • 7: 0.00000094

  • 8: 0.00000006

  • more: less than 1 in ten million

TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转化成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin,并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。

这样就解释了不是一开始就将其转换成TreeNodes而是需要一定节点才转成TreeNodes,说白了就是空间和时间的权衡,红黑树节点占用空间比较大,如果一开始就是用红黑树,就会消耗大量的空间资源

这段内容还说到:当hashCode离散性很好的时候,树形bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件,所以之所以选择8,不是随便决定的,而是根据概率统计决定的。

补充:

泊松分布

在这里插入图片描述

泊松分布的参数在这里插入图片描述

是单位时间(或单位面积)内随机事件的平均发生次数。泊松分布适合于描述单位时间内随机事件发生的次数

2.以下是我在研究问题时,在一些资料上面翻看的为什么长度为8时链表进化为红黑树,长度小于等于6时红黑树又退化成链表的解释:供大家参考

红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,

平均查找长度为4,此时才有转换成树的必要,链表长度如果是小于等于6,链表的平均查找长度为6/2=3,而红黑树此

时为log(6)=2.6,虽然速度也很快

但是转化为树结构和生成树的时间并不会太短,两者此时所用时间相差无几

6.当链表中的值小于6则会从红黑树转回链表

//当桶(bucket)上的结点数小于这个值时树转链表

static final int UNTREEIFY_THRESHOLD = 6;

7.链表转红黑树时数组最大容量

当Map里面的数量超过这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化,为了避免进行扩容、树形化的选择的冲突,这个值不能小于4*TREEIFY_THRESHOLD(8)

//桶中结构转化为红黑树对应的数组长度最小的值

static final int MIN_TREEIFY_CAPACITY = 64;

8.table用来初始化(必须是二的n次幂)(重点)

//存储元素的数组

transient Node<K,V>[] table;

在JDK1.8中我们了解到HashMap是由数组+链表+红黑树来组成的结构,其中table就是HashMap中的数组,jdk8之前数组类型是Entry<K,V>类型。从jdk1.8之后是Node<K,V>类型。只是换了个名字,都实现了一样的接口:Map.Entry<K,V>,负责存储键值对数据

9.存放缓存

//存放具体元素的集合

transient Set<Map.Entry<K,V>> entrySet;

10.HashMap中存放元素的个数(重点)

//存放元素的个数,注意这个不等于数组的长度

transient int size;

size为HashMap中K-V的实时数量,不是数组table的长度

11.用来记录HashMap的修改次数

//每次扩容和更改map结构的计数器

transient int modCount;

12.用来调整大小下一个容量的值,计算方式为(容量*负载因子)

//临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容

int threshold;

13.哈希表的加载因子(重点)

//加载因子

final float loadFactor;

loadFactor说明:

  • loadFactory加载因子,是用来衡量HashMap满的程度,表示HashMap的疏密程度,影响hash操作到同一个数组位置的概率,计算HashMap的实施加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。capacity是桶的数量,capacity是桶的数量,也就是table的长度length

  • - loadFactory太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactory的默认值为0.75f是官方给出的一个比较好的临界值

  • 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要进行扩容,而扩容这个过程涉及到rehash,复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免

  • 同时在HashMap的构造器中可以定制loadFactory

  • public HashMap(int initialCapacity, float loadFactor) //构造一个带指定初始容量和加载因子的空HashMap

为什么加载因子设置为0.75,初始化临界值是12?

=======================================================================================

loadFactory越趋近于1,那么数组中存放的数据(entry也就越多),也就越密集,也就会有更多的链表长度处于一个更长的数值,导致查询效率降低,每当我们添加数据,产生hash冲突的概率也会更高

loadFactory越小,也就是越趋近于0,数组中存放的数据(entry)也就越少,表现得更加稀疏

在这里插入图片描述

如果希望链表尽可能少些,要提前扩容,有的数组空间有可能一直没有存储数据,加载因子尽可能小一些

  • 加载因子是0.4,那么16*0.4=6如果数组中满6个空间就扩容,很有可能许多空间内并没有元素或元素很少,会造成大量的空间浪费

  • 加载因子是9,16*0.9=14,这样就会导致扩容之前查找元素的效率很低

在这里插入图片描述

HashMap构造方法

=========================================================================

HashMap中最重要的构造方法,他们分别如下:

HashMap()


构造一个空的HashMap,默认初始容量(16)和默认负载因子(0.75)

public HashMap() {

this.loadFactor = DEFAULT_LOAD_FACTOR; // 将默认的加载因子0.75赋值给loadFactory,并没有创建数组

}

HashMap(int initialCapacity)


构造一个具有指定的初始容量和默认负载因子的HashMap

public HashMap(int initialCapacity) {

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

MySQL全家桶笔记

还有更多面试复习笔记分享如下

Java架构专题面试复习

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
oadFactory,并没有创建数组

}

HashMap(int initialCapacity)


构造一个具有指定的初始容量和默认负载因子的HashMap

public HashMap(int initialCapacity) {

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

[外链图片转存中…(img-wLZ1I7bA-1713475421992)]

还有更多面试复习笔记分享如下

[外链图片转存中…(img-xsUhaeEg-1713475421992)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-haT8KPo4-1713475421993)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值