5千字长文,深度总结HashMap底层实现&面试题【收藏】,java高级程序员面试笔试宝典 pdf

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

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

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

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

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

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

正文

  • 删除元素

map1.remove(“TOM”);//根据键,去删除键值对

map1.clear(); //删除map1的所有元素

  • 元素个数

int size = map1.size(); //返回当前集合中的键值对数量

  • 查找是否包含键(Key)

//查找map1是否有TOM这个键(Key)

boolean isContainsK = map1.containsKey(“TOM”);

  • 查找是否包含值(Value)

//查找map1是否有猫,这个值(Value)

boolean isContainsV = map1.containsValue(“猫”);

  • map转Set

//将map集合,转换为Set类型的集合

Set<Map.Entry<String, String>> entry = map1.entrySet();

以上方法,都好理解,主要就是最后一个Map转为Set的这个方法,我们讲解一下。

因为Set集合,它只是存储一个类型的元素,而不像HashMap这样是键值对。所有当HashMap转为Set时,会将HashMap的键和值,打包成一个整体(Map.Entry<K,V>),打包后的对象类型就是这个括号里面的。然后再将打包好的整体放入Set集合中,如下图所示:

image-20211102151628107

就如上图,打包成Map.Entry<K,V>类型,再放入Set集合中。这样做的好处就是,可以使用迭代器(Iterator)进行遍历,因为Map接口是没实现Interable接口的,无法使用迭代器,只能转为Set集合,再使用迭代器。以下是使用迭代器的代码:

Set<Map.Entry<String, String>> entry = map1.entrySet();

for (Map.Entry<String,String> set : entry) {

//调用get方法,即可拿到键和值

String key = set.getKey();

String value = set.getValue();

}

还有值得注意的是,键重复的情况,假设目前HashMap中已经有了(“TOM”,“猫”)这一对键值。如果此时我再添加元素(“TOM”,“狗”),因为键是一样的,则会找到表中TOM的位置,将“狗”放入了表中,实现了覆盖的效果。如下代码:

HashMap<String,String> map1 = new HashMap<>();

map1.put(“TOM”, “猫”); //第一次放入

map1.put(“TOM”, “狗”); //第二次方法,则会将猫改写为狗

二、哈希函数

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

1、哈希函数的概念


在我们前面学习的所有数据结构中,无论是二叉树,还是链表,在增删查改的操作上,时间复杂度都还是有那么一点点不尽人意,拿搜索二叉树举例,当插入的数据有序时,会退化成链表,从而产生了平衡树。但平衡树的增删查改也只能做到O(logN)的水平,没法再快了。

但哈希函数可以做到O(1)的水平,这是一个非常恐怖的概念,增删查改都能做到O(1)的水平,快了不止多少倍。那么它到底是怎么实现的?我们来简单分析一下:

哈希函数:根据给定的数据,进行计算,得到一个地址,在这个地址的地方进行放入元素。

取出元素的时候,也只是根据数据计算一个地址,在该地址处取出元素。

这就是哈希函数简单的认识。就是根据一些计算,推导出一个地址。

比如:hash = 元素 % 10; 这样一个最简单的哈希函数公式,假设我们传递的数据,是99,则99 % 10 = 9。则在数组下标为9的位置放入数据……

但是还有一个问题就是:假设现在传递的数据是89,89 % 10 = 9。还是在下标为9 的位置进行插入数据。此时就发生了意外,当前9下标位置已经有数据了。

image-20211102154749506

以上这样的情况,就称为哈希冲突不同的关键字,根据相同的哈希函数计算出相同的哈希值,就是哈希冲突

人们可以设计出精妙的哈希函数,来降低哈希冲突的概率,但是绝对不可能做到不发生冲突。也就是说,但我们添加元素足够多时,发生哈希冲突,是一个必然的现象

常见的哈希函数,我们这里就不列举了,大家查一下就行。大多数人都不会去设计哈希函数,我们只需知道有这么个东西即可。

2、哈希冲突,该怎么办?


为了尽量降低哈希冲突,我们就得从两个方面着手:在发生冲突之前和发生冲突之后

发生冲突之前:设计出精妙的哈希函数,存放数据的内存空间开辟大一点,还有就是有一个负载因子的概念。

负载因子image-20211102155511736

也就是表中的元素除以哈希表的长度,就是负载因子。

负载因子的曲线图大致如下:

也就是说在负载因子达到一个界限的时候,就需要进行扩容,将数组扩大,即可降低负载因子。

以上方案只是在尽量预防发生冲突,那如果真的发生冲突了,又该怎么办?

哈希冲突后

常见的解决哈希冲突的方法是:闭散列开散列

闭散列:也叫开放地址法,当发生哈希冲突的时候,说明此当前下标位置已经有数据了,此时我们只需沿着当前位置往下查找一个没放数据的位置,插入数据即可,如下:

image-20211102161130742

在插入89时,发生了冲突,就在9下标的后面找到一个空位置插入数据,但是9下标后面没位置了,则从数组前面开始找。

那有人可能就会问,会不会整个数据都已经满了呢??

不可能的!因为有一个负载因子在控制数组的大小,当负载因子达到一个值的时候,就会进行扩容。

以上的方法就称为线型探测法。沿着下标,一直往下找,找到一个空位就插入即可。但是这样做的不好之处就在于,发生冲突的所有数据都聚集在一块地方,对删除操作增加了不小的难度。

例如:假设现在需要删除99,那能直接将99拿走吗?如果拿走了,那89,又如何通过哈希函数取出呢??

所有线型探测法,对于删除操作,采用了伪删除法,也就是说,有一个变量记录99的状态表示已经删除了。

除了线型探测,还有二次探测法,就是为了解决冲突的元素聚集在一块这个问题:二次探测法,将不在沿着冲突位置的下标一个一个往下找空位置,而是通过一个公式:待插入下标 = (冲突下标 + i 2)% m 。此处的i就是发生冲突的次数。比如89第一次插入时,会跟99第一次发生冲突,计算下一个下标后,发现下一位置还是冲突的,则i=2,一直往下推导……

最重要的就是开散列

开散列:又叫链地址法。将发生冲突的元素,用一个单链表进行串连起来,将头节点,放入数组中即可。如下图:

image-20211102162748438

将所有发生冲突的元素,用一个单链表串连起来即可。然后在这个单链表中一个个查找。

但是还是会出现意外,如果单链表的长度很长的话,还是做不到O(1)的时间复杂度,那么此时就需要将这个单链表,转换为一个哈希表,或者是建一个红黑树(JDK1.8之后)的形式。

以上两种方法,就是解决哈希冲突之后的方案,特别值得记住的就是开散列。

3、JDK1.8的HashMap源码


(1)构造方法

在JDK1.8之前,HashMap只是采用数组+单链表的形式进行存储,且当时的单链表是用头插法的形式插入节点的

在JDK1.8之后,HashMap采用数组+单链表+红黑树的形式进行存储,且单链表是用尾插法的形式插入节点

那么是怎么进行存储的,我们来简单分析一下源码:

image-20211102165142786

无参构造方法,只是设置了负载因子,并没有设置HashMap底层数组的大小。所以,只调用构造方法,不调用put方法,数组大小是0。跟ArrayList是一样的,ArrayList在调用add时,会将数组大小设置为10。

有参构造方法:

image-20211102170309139

切记:HashMap底层数组的大小,一定是2的某次方,假设调用方传递的是20,这个数并不是2的某次方,则会向上调整,取离20最近的,且还是2的某次方的数。这里的话,离20最近且还是2的某次方的数,就是32。

千万注意,HashMap底层数组大小,不一定就是new的时候的初始化容量

其次,我们需要认识一下几个常见的成员变量,以便于我们后续读懂put方法。

image-20211103160708922

(2)put方法

在读put方法前,一定需要理解上图中的几个成员变量的意义,然后再来读put方法,这样才不至于越看越懵。

  1. image-20211103161045484

首先第一步就是put方法,调用了putVal方法 。这里需注意hash函数,设计的比较精妙。

image-20211103161250091

hash函数,除了调用了hashCode方法以外,还将计算出来的hashCode的值右移了16位,然后再做与运算。因为hashCode放回的是int类型,右移16位是将这个数值的每一位都利用起来

  1. putVal方法

image-20211103161817710

tab指向成员变量table数组,n就是当前数组的长度。

首先我们先来看第一条逻辑分支,628行。tab=table,首先是判断是否为null,第二判断就是数组的长度等于0的时候。当调用无参构造方法时,没有为数组分配大小和赋值等操作,所有此时调用put方法是,第628行的逻辑判断就是

第629行,tab=resize()。人如其名,resize就是调整数组大小的。这个方法有两个作用:初始化数组大小扩容

那么如何才能正确的掌握Redis呢?

为了让大家能够在Redis上能够加深,所以这次给大家准备了一些Redis的学习资料,还有一些大厂的面试题,包括以下这些面试题

  • 并发编程面试题汇总

  • JVM面试题汇总

  • Netty常被问到的那些面试题汇总

  • Tomcat面试题整理汇总

  • Mysql面试题汇总

  • Spring源码深度解析

  • Mybatis常见面试题汇总

  • Nginx那些面试题汇总

  • Zookeeper面试题汇总

  • RabbitMQ常见面试题汇总

JVM常频面试:

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Mysql面试题汇总(一)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Mysql面试题汇总(二)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Redis常见面试题汇总(300+题)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

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

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

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

Mysql面试题汇总(一)

[外链图片转存中…(img-LGWtclrK-1713381568340)]

Mysql面试题汇总(二)

[外链图片转存中…(img-1LNUN026-1713381568340)]

Redis常见面试题汇总(300+题)

[外链图片转存中…(img-hpxJ4eYl-1713381568340)]

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值