先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
面试题1:哈希表底层采用何种算法计算hash值?还有哪些算法可以计算出hash值?
- 底层采用的是key的hashCode方法的值结合数组长度进行无符号右移(>>>),按位异或(^),按位与(&)计算出索引。还可以采用平方取中法,取余数,伪随机数法
面试题2:当两个对象的hashCode相等时会怎么样?
- 会产生hash碰撞,若key值内容相等则替换旧的value,反之则连接到链表后面,链表长度超过阈值8就转换成红黑树存储
面试题3:何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞
- 只要两个元素的key计算的hash码值相同就会发生hash碰撞,jdk8前使用链表解决哈希碰撞,jdk8之后使用链表+红黑树解决哈希碰撞
面试题4:如果两个键的hashcode相同,如何存储键值对
- hashcode相同则会调用equals比较内容是否相同,如果相同则会用新value值覆盖老value值,如果不相同则会连接到链表后面
============================================================================
从图中可以看出HashMap实现了Map,Cloneable,Serializable接口
-
Cloneable空接口,表示可以克隆,创建并返回HashMap对象的一个副本。
-
Serializable序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化
-
AbstracMap父类提供了Map实现接口,以最大限度的减少实现此接口所需的工作
知识补充:
通过上述继承关系我们发现一个很奇怪的现象,HashMap已经继承了AbstractMap而AbstractMap已经实现了Map接口,那为什么HashMap还要再实现Map接口呢?同样在ArrayList中LinkedList中都是这种结构
据java集合框架的创始人Josh Bloch描述,这样的写法是一个失误,在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识错了。显然的,JDK的维护者后来不认为这个小小的失误值得去修改,所以就这样存在下来了
代码如下(示例):
===========================================================================
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;
======================================================================================
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
=======================================================================================
loadFactory越趋近于1,那么数组中存放的数据(entry也就越多),也就越密集,也就会有更多的链表长度处于一个更长的数值,导致查询效率降低,每当我们添加数据,产生hash冲突的概率也会更高
loadFactory越小,也就是越趋近于0,数组中存放的数据(entry)也就越少,表现得更加稀疏
如果希望链表尽可能少些,要提前扩容,有的数组空间有可能一直没有存储数据,加载因子尽可能小一些
-
加载因子是0.4,那么16*0.4=6如果数组中满6个空间就扩容,很有可能许多空间内并没有元素或元素很少,会造成大量的空间浪费
-
加载因子是9,16*0.9=14,这样就会导致扩容之前查找元素的效率很低
=========================================================================
HashMap中最重要的构造方法,他们分别如下:
构造一个空的HashMap,默认初始容量(16)和默认负载因子(0.75)
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 将默认的加载因子0.75赋值给loadFactory,并没有创建数组
}
构造一个具有指定的初始容量和默认负载因子的HashMap
public HashMap(int initialCapacity) {
最后
由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档
还有更多面试复习笔记分享如下
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
oadFactory,并没有创建数组
}
构造一个具有指定的初始容量和默认负载因子的HashMap
public HashMap(int initialCapacity) {
最后
由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档
[外链图片转存中…(img-wLZ1I7bA-1713475421992)]
还有更多面试复习笔记分享如下
[外链图片转存中…(img-xsUhaeEg-1713475421992)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-haT8KPo4-1713475421993)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!