收集大量Java经典面试题目📚,内容涵盖了包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 等知识点🏝️。适合准备Java面试的读者参考和复习🌟📢。
❗ ❗ ❗
关注公众号:枫蜜柚子茶 ✅✅
🗳
📑 回 复 “ Java面试 ” 获 取 完 整 资 料⬇ ⬇ ⬇
📖Java集合类面试题目Top52道题🔥🔥
1️⃣ Java 集 合 容 器 概 述
2️⃣ Collection 接 口
3️⃣ Map 接 口 🚩
4️⃣ 辅 助 类 工 具
一、Map接口
1. 什么是Hash算法 ❓❗
哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
2. 什么是链表
链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查 等功能。
📛链表大致分为单链表和双向链表:
1. 单链表: 每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针。
2. 双向链表: 除了包含单链表的部分,还增加的pre前一个节点的指针。
✅链表的优点:
- ◾ 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加 删除元素)。
- ◾ 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间)。
- ◾ 大小没有固定,拓展很灵活。
- ◾ 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间)。
❌ 链表的缺点:
◾ 不能随机查找,必须从第一个开始遍历,查找效率低
3. 说一下HashMap的实现原理❓❗
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射 操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
📝 HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
- 1️⃣. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数 组中的下标
- 2️⃣. 存储时,如果出现hash值相同的key,此时有两种情况。
- (1)如果key相同,则覆盖原始值;
- (2)如果key不同(出现冲突),则将当前的key-value放入链表中。
- 3️⃣. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
- 4️⃣. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的 存储方式,然后将冲突的key的对象放入链表中, 一旦发现冲突就在链表中做进一步的对比。
4. HashMap在JDK1.7和JDK1.8中有哪些不同? HashMap的底层实现
在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和 删除困难;链表的特点是:寻址困难,但插入和删除容易; 所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。
HashMap JDK1.8之前
JDK1.8之前采用的是拉链法。 拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组 中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
HashMap JDK1.8之后
相比于之前的版本, jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时,将链表转化为红黑树,以减少搜索时间。
JDK1.8主要解决或优化了一下问题:
- 1. resize 扩容优化
- 2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
- 3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同 | JDK 1.7 | JDK 1.8 |
存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
初始化方 式 | 单独函数: inflateTable() | 直接集成到了扩容函数 resize() 中 |
hash值 计算方式 | 扰动处理 = 9次扰动 = 4次位运 算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异 或运算 |
存放数据 的规则 | 无冲突时,存放数组;冲突 时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8: 树化并存放红黑树 |
插入数据 方式 | 头插法(先讲原位置的数据移 到后1位,再插入数据到该位 置) | 尾插法(直接插入到链表尾部/红黑树) |
扩容后存 储位置的 计算方式 | 全部按照原来方法进行计算 (即hashCode ->> 扰动函数 - >> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置 =原位置 or 原位置 + 旧容量) |
5. 什么是红黑树
说道红黑树先讲什么是二叉树
◾ 二叉树简单来说就是 每一个节上可以关联俩个子节点
a
/ \
b c
/ \ / \
d e f g
/ \ / \ / \ / \
h i j k l m n o
红黑树
红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红 (Red)或黑(Black)。
红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点! ]。
如果一个结点是红色的,则它的子结点必须是黑色的。
. 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。 [确保没有一条路径会比其他路径长出俩 倍,所以红黑树是相对接近平衡的二叉树的! ]
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么
呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面 三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新 成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。
6. HashMap的put方法的具体流程?
当我们put的时候,首先计算 key 的 hash值,这里调用了 hash方法, hash方法实际是让key.hashCode() 与 key.hashCode()>>>16进行异或操作,高16bit补0 ,一个数和0异或不变, 所以 hash 函数大概的作用就是: 高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者 综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中 用了复杂度 O(logn)的树结构来提升碰撞下的性能。
. putVal方法执行流程图
- 1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
- 2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向
- ⑥,如果table[i]不为空,转向③;
- 3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
- 4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
- 5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
- 6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
7. HashMap的扩容操作是怎么实现的?
- 1. 在jdk1.8中, resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
- 2. 每次扩展的时候,都是扩展2倍;
- 3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
在putVal()中,我们看到在这个函数里面使用到了2次resize()方法, resize()方法表示的在进行第一 次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩 容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据 在同一个桶的位置中进行判断(e.hash& oldCap)是否为0,重新进行hash分配后,该元素的位置 要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上。
8. HashMap是怎么解决哈希冲突的?
✅答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知 道什么是哈希才行;
什么是哈希?
Hash ,一般翻译为“散列” ,也有直接音译为 “哈希”的, Hash就是指使用哈希算法是指把任意长度 的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希 碰撞)。
HashMap的数据结构
在Java中,保存数据有两种比较简单的数据结构:数组和链表。
- ◾ 数组的特点是:寻址容易,插入和删除困难;
- ◾ 链表的特点是:寻址困难,但插入和删除容易;
所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放 地址法可以解决哈希冲突:
- ◾ 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
- ◾ 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽 位。
- ◾ 但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4 (即2的四次方16)要远小于int类型的范围,所以我们 如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最 坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化。
hash()函数
◾ 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算, 进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的 hash()函数如下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己 右移16位进行异或运算(高低位异或) } |
📛 这比在JDK 1.7中,更为简洁, 相比在1.7中的4次位运算, 5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动) ;
总结💡:
◾ 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
◾ 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
9. 能否使用任何类作为 Map 的 key?
🔻 可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:
- 🔹 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
- 🔹 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。 . 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
- 🔹 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的 性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。
- 🔹 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。 . 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
10. 为什么HashMap中String、 Integer这样的包装类适合作为K?
✅ 答:String、 Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减 少Hash碰撞的几率.
- ◾ 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
- ◾ 内部已重写了 equals() 、 hashCode()等方法,遵守了HashMap内部的规范(不清楚可以 去上面看看putValue的过程),不容易出现Hash值计算错误的情况;