先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
其中关于HashMap的考察和提问在面试中是最频繁的,这也是在日常开发中最应该深入理解和掌握的。所以接下来就主要和大家详细分析一下HashMap的实现原理以及面试中的常考问题。
3、请阐述HashMap的put过程?
=======================
我们知道HaahMap使用put的方式进行数据的存储,其中有两个参数,分别是key和value,那么关于这个键值对是如何进行储存的呢?我们接下来进行分析一下。
在HashMap中使用的是数组+链表的实现方式,在HashMap的上层使用数组的形式对“相同”的key进行存储,下层对相应的key和value使用链表的形式进行链接和存储。
**注意:这里所说的相同并不一定是key的数值相同,而是存在某种相同的特征,**具体是哪种特征骂我们继续往下看!
HashMap将将要存储的值按照key计算其对应的数组下标,如果对应的数组下标的位置上是没有元素的,那么就将存储的元素存放上去,但是如果该位置上已经存在元素了,那么这就需要用到我们上面所说的链表存储了,将数据按照链表的存储顺序依次向下存储就可以了。这就是put的简单过程,存储结果如下:
但是我们有时候存储的数据会很多,那么如果一直使用链表的形式进行数据的存储的话就或造成我们的链表的长度非常大,这样无论在进行删除还是在进行插入操作都是十分麻烦的,因此对于这种情况应该怎么办呢?
**这里就涉及到了一个链表中数据存储时,进行“树化”和“链化”的一个过程,**那么什么是“树化”和“链化”呢?
当我们在对键值对进行存储的时候,如果我们在同一个数组下标下存储的数据过多的话,就会造成我们的链表长度过长,导致进行删除和插入操作比较麻烦,所以在java中规定,**当链表长度大于8时,我们会对链表进行“树化”操作,****将其转换成一颗红黑树(一种二叉树,左边节点的值小于根节点,右边节点的值大于根节点),**这样我们在对元素进行查找时,就类似于进行二分查找了,这样的查找效率就会大大增加。
但是当我们进行删除操作,将其中的某些节点删除了之后,链表的长度不再大于8了,这个时候怎么办?难道就要赶紧将红黑树转化为链表的形式吗?其实并不是,只有当链表的长度小于6的时候,我们才会将红黑树重新转化为链表,这个过程就叫做“链化”。
过程图示如下:
那么为什么要在长度8的时候进行“树化”,而在长度小于6的时候才进行“链化”呢?为什么不直接在长度小于8的时候就进行“链化”?
**主要原因是因为:**当删除一个元素,链表长度小于8的时候直接进行“链化”,而再增加一个元素,长度又等于8的时候,又要进行“树化”,这样反复的进行“链化”和“树化”操作特别的消耗时间,而且也比较麻烦。所以程序就规定,只有当当链表长度大于等于8的时候才进行“树化”,而长度小于6的时候才进行“链化”,其中关于8树化、6链化这两个阈值希望大家牢记!
4、链表中是按照怎样的顺序存放数据的?
=======================
我们现在已经知道了HashMap中的元素是如何存放的,但是有时候面试官可能还会问我们,在HashMap中,向链表中存储元素是在头结点存储的还是在尾节点存储的?
这个我们需要知道,对于HashMap中链表元素的存储。
在JDK1.7以及前是在头结点插入的,在JDK1.8之后是在尾节点插入的。
5、Hash(key)方法是如何实现的?
========================
我们现在已经知道了HashMap中的元素是如何存储的了,那么现在就是如何应该根据key值进行相应的数组下标的计算呢?
我们知道HashMap的初始容量是16位,那么对于初始的16个数据位,如果将数据按照key的值进行计算存储,一般最简单的方法就是根据key值获取到一个int值,方法是:
int hashCode = key.hashCode()
然后对获取到的hashCode与16进行取余运算,
hashCode % 16 = 0~15
这样得到的永远都是0—15的下标。这也是最最原始的计算hash(key)的方法。
**但是在实际情况下,这种方法计算的hash(key)并不是最优,**存放到数组中的元素并不是最分散的,而且在计算机中进行余运算其实是非常不方便的、
**所以为了计算结果尽可能的离散,现在计算数组下标最常用的方法是:**先根据key的值计算到一个hashCode,将hashCode的高16位二进制和低16位二进制进行异或运算,得到的结果再与当前数组长度减一进行与运算。最终得到一个数组下标,过程如下:
int hashCode = key.hashCode()
int hash = hash(key) = key.hashCode()的高16位^低16位&(n-1) 其中n是当前数组长度
同时在这里要提醒一点。
在JDK1.7和JDK1.8的时候对hash(key)的计算是略有不同的
JDK1.8时,计算hash(key)进行了两次扰动
JDK1.7时,计算hash(key)进行了九次扰动,分别是四次位运算和五次异或运算
其中扰动可能理解为运算次数
以上就是Hash(key)方法的实现过程。
6、为什么HashMap的容量一直是2的倍数?
===========================
HashMap的容量之所以一直是2的倍数,其实是与上面所说的hash(key)算法有关的,
原因是只有参与hash(key)的算法的(n-1)的值尽可能都是1的时候,得到的值才是离散的。假如我们当前的数组长度是16,二进制表示是10000,n-1之后是01111,使得n-1的值尽可能都是1,对于其他是2的倍数的值减1之后得到值也是这样的。
所以只有当数组的容量长度是2的倍数的时候,计算得到的hash(key)的值才有可能是相对离散的,
7、Hash冲突如何解决?
=================
什么是Hash冲突?就是当我计算到某一个数组下标的时候,该下标上已经存放元素了,这就叫Hash冲突,很显然,如果我们计算数组下标的算法不够优秀的时候,很容易将存储的数据积累到同一个下标上面,造成过多的Hash冲突。
那么如何解决hash冲突?
最应该解决的其实就是让存储的key计算得到的数组下标尽可能的离散,也就是要求hash(key)尽可能的优化,数组长度是2的倍数。这也就是Hash冲突的主要解决方法。
具体可以查看下面HashMap关键部分的底层源码:
Hash(key)的底层实现
/**
-
Applies a supplemental hash function to a given hashCode, which
-
defends against poor quality hash functions. This is critical
-
because HashMap uses power-of-two length hash tables, that
-
otherwise encounter collisions for hashCodes that do not differ
-
in lower bits. Note: Null keys always map to hash 0, thus index 0.
*/
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
put(key,value)方法的底层实现
/**
-
Associates the specified value with the specified key in this map.
-
If the map previously contained a mapping for the key, the old
-
value is replaced.
-
@param key key with which the specified value is to be associated
-
@param value value to be associated with the specified key
-
@return the previous value associated with key, or
-
<tt>null</tt> if there was no mapping for <tt>key</tt>.
-
(A <tt>null</tt> return can also indicate that the map
-
previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
8、HashMap是如何扩容的?
====================
我们在上面说到了HashMap的数组的初始容量是16,但是很显然16个存储位是显然不够的,那么HashMap应该如何扩容呢?
在这里需要用到一个参数叫“扩容因子”,在HashMap中“扩容因子”的大小是0.75,
**我们上面也提到过,对于初始长度为16的数组,当其中存储的数据长度等于16*0.75=12时。就会对数组元素进行扩容,扩容量是原来数组容量的2倍,**也就是当前是15话,再扩容就是扩容32个数据位。
9、扩容后元素怎么存放的?
=================
2021年Java中高级面试必备知识点总结
在这个部分总结了2019年到目前为止Java常见面试问题,取其面试核心编写成这份文档笔记,从中分析面试官的心理,摸清面试官的“套路”,可以说搞定90%以上的Java中高级面试没一点难度。
本节总结的内容涵盖了:消息队列、Redis缓存、分库分表、读写分离、设计高并发系统、分布式系统、高可用系统、SpringCloud微服务架构等一系列互联网主流高级技术的知识点。
目录:
(上述只是一个整体目录大纲,每个点里面都有如下所示的详细内容,从面试问题——分析面试官心理——剖析面试题——完美解答的一个过程)
部分内容:
对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。
不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。
不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-qMKqQZvB-1713436943482)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!