注意:
如果本文中有错误的地方,欢迎评论区指正!🍭
文章目录
-
- 1.说说Java中常用的容器有哪些?
-
2.详细说说 Arraylist 和 LinkedList的区别?
-
3.ArrayList实现 RandomAccess接口有何作用?
-
4.说一说Vector 和 ArrayList 的区别?
-
5.说说ArrayList 的扩容机制?
-
6.Array和ArrayList有何区别?
-
7.遍历一个List有哪些不同的方式?
-
8.comparable和comparator的区别?
-
9.Collection和Collections有什么区别?
-
10.说一下PriorityQueue?
-
11.说一下HashSet的实现原理?
-
12.HashMap的实现原理/底层数据结构?
-
13.HashMap 的长度为什么是 2 的幂次方?
-
14.说说HashMap的put方法执行流程?
-
15.说说HashMap的get方法执行流程?
-
16.说说HashMap的resize方法执行过程?
-
17.HashMap什么时候会树化?
-
18.HashMap底层为什么选择红黑树而不用其他树,比如二叉查找树?
-
19.HashMap扩容(加载)因子为何默认是 0.75f?
-
20.HashMap怎么计算 key 的 hash 值的?
-
21.HashMap是怎么解决哈希冲突的?
-
22.HashMap多线程操作导致死循环问题知道吗?
-
23.说说LinkedHashMap 的实现原理?
-
24.说说HashMap 和 HashSet 区别?
-
25.说下HashMap 和 Hashtable 的区别?
-
26.说一下HashMap 和 TreeMap 区别?
-
27.为什么HashMap中String、Integer这样的包装类适合作为Key?
-
28.说一下Queue 与 Deque 的区别?
-
29.说说ArrayDeque 与 LinkedList 的区别?
-
30.说一下 HashSet、LinkedHashSet 和 TreeSet 三者的异同?
容器主要包括 Collection
和 Map
两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
如图:
👨💻面试官追问:说说集合有哪些类及他们各自的区别和特点?
-
Set
-
TreeSet
基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。 -
HashSet
基于HashMap实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 -
LinkedHashSet
是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。内部使用双向链表维护元素的插入顺序。 -
List
-
ArrayList
基于动态数组实现,支持随机访问。 -
Vector
和 ArrayList 类似,但它是线程安全的。 -
LinkedList
基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 -
Queue
-
LinkedList
可以用它来实现双向队列。 -
PriorityQueue
基于堆结构实现,可以用它来实现优先队列。 -
ArrayQueue
基于数组实现,可以用它实现双端队列,也可以作为栈。
👨💻面试官追问:说说Map有哪些类及他们各自的区别和特点?
-
TreeMap
基于红黑树实现。 -
HashMap
1.7基于数组+链表实现,1.8基于数组+链表+红黑树。链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 -
HashTable
和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。(现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高(1.7 ConcurrentHashMap 引入了分段锁, 1.8 引入了红黑树)。) -
LinkedHashMap
继承自 HashMap。使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
2.详细说说 Arraylist 和 LinkedList的区别?
-
ArrayList
:底层是基于数组实现的,查找快,增删较慢。LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 -
LinkedList
:底层是基于链表实现的。确切的说是循环双向链表(JDK1.6之前是双向循环链表、JDK1.7之后取消了循环),查找慢、增删快。LinkedList链表由一系列表项连接而成,一个表项包含3个部分︰元素内容、前驱表和后驱表。因此内存空间占用比ArrayList 更多。
👨💻面试官追问:ArrayList的增删一定比LinkedList要慢吗?
不一定的。
-
如果增删都是在末尾来操作(每次调用的都是
remove()
和add()
),此时 ArrayList就不需要移动和复制数组来进行操作了。如果数据量有百万级的时,速度是会比 LinkedList 要快的。 -
如果删除操作的位置是在中间。由于LinkedList的消耗主要是在遍历上,ArrayList的消耗主要是在移动和复制上(底层调用的是
arrayCopy()
方法,是native方法)。LinkedList 的遍历速度是要慢于ArrayList的复制移动速度的。如果数据量有百万级的时,还是ArrayList要快。
3.ArrayList实现 RandomAccess接口有何作用?
public interface RandomAccess {
}
查看源码我们发现实际上 RandomAccess
接口中什么都没有定义。
从源码可以看出RandomAccess 接口只是一个标志接口,只要List集合实现这个接口,就能支持快速随机访问。通过查看Collections
类中的binarySearch()
方法,可以看出,判断List是否实现RandomAccess接口来实行indexedBinarySerach(list, key)
或 iteratorBinarySerach(list, key)
方法。再通过查看这两个方法的源码发现:实现RandomAccess接口的List集合采用一般的 for循环遍历,而未实现这接口则采用迭代器,即ArrayList 一般采用for循环遍历,而 LinkedList 一般采用迭代器遍历
public static
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
👨💻面试官追问:为何LinkedList却没实现这个接口?
ArrayList
底层是数组,而 LinkedList
底层是链表。
数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。
ArrayList
实现了 RandomAccess
接口,就表明了他具有快速随机访问功能。 RandomAccess
接口只是标识,并不是说 ArrayList
实现 RandomAccess
接口才具有快速随机访问功能的!
他们两个都实现了List
接口。底层数据结构都是数组。
不同的是:
-
vector通过
remove
、add
等方法加上synchronized
关键字实现线程同步,所以是线程安全的。而ArrayList是线程不安全的 -
由于vector使用了
synchronized
进行加锁,所以性能不如ArrayList -
Vector 扩容时,如果未指定扩容递增值
capacityIncrement
,或该值不大于 0 时,每次扩容为原来的2
倍,否则扩容量为capacityIncrement
的值。ArrayList每次扩容为旧容量的1.5
倍
-
当使用add方法的时候首先调用
ensureCapacityInternal
方法,传入size+1
进去,检查是否需要扩容 -
如果空数组
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,就初始化为默认大小10,获取“默认的容量”和要扩容的大小两者之间的最大值 -
和当前数组长度比较,如果
if (minCapacity - elementData.length > 0)
执行grow
扩容方法 -
将数组扩容为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
-
检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量
-
再检查新容量newCapacity 是否超出了ArrayList所定义的最大容量,若超出了,则调用
hugeCapacity()
来比较minCapacity和 MAX_ARRAY_SIZE,如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE(MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
) -
ArrayList 中copy数组的核心就是
System.arraycopy
方法,将original数组的所有数据复制到copy数组中,这是一个本地方法
详细的扩容源码可以参考:https://blog.csdn.net/qq_45966440/article/details/122270715?spm=1001.2014.3001.5501
-
Array可以容纳基本类型和对象,而ArrayList只能容纳对象
-
Array是指定大小的,ArrayList 的容量是根据需求自动扩展
-
ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等
👨💻面试官追问:什么时候更适合使用Array?
-
如果列表的大小已经指定,大部分情况下是存储和遍历它们可以使用Array
-
对于基本类型数据,ArrayList 使用自动装箱来减少编码工作量;而当处理固定大小的基本数据类型的时候,这种方式相对比较慢,这时候应该使用Array
-
如果你要使用多维数组,使用
[][]
比 List更容易
先说一下常见的元素在内存中的存储方式,主要有两种:
-
顺序存储(Random Access):相邻的数据元素在内存中的位置也是相邻的,可以根据元素的位置读取元素。
-
链式存储(Sequential Access):每个数据元素包含它下一个元素的内存地址,在内存中不要求相邻。例如LinkedList。
主要的遍历方式主要有三种:
-
for
循环遍历:遍历者自己在集合外部维护一个计数器,依次读取每一个位置的元素 -
Iterator
遍历:基于顺序存储集合的Iterator可以直接按位置访问数据。基于链式存储集合的Iterator,需要保存当前遍历的位置,然后根据当前位置来向前或者向后移动指针 -
foreach
遍历:也就是增强for循环,foreach内部也是采用了Iterator的方式实现,但使用时不需要显示地声明Iterator
代码如下:
public class TestLinkedList {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(“aaa”);
list.add(“bbb”);
list.add(“ccc”);
//for循环遍历
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
//Iterator遍历
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//foreach遍历
for (String s : list) {
System.out.println(s);
}
}
}
👨💻面试官追问:那么对于以上三种遍历方式应该如何选取呢?
在Java集合框架中,提供了一个RandomAccess
接口,该接口没有方法,只是一个标记。通常用来标记List的实现是否支持RandomAccess。所以在遍历时,可以先判断是否支持RandomAccess ( list instanceof RandomAccess),如果支持可用for循环遍历,否则建议用Iterator或 foreach遍历。
comparable
接口出自java.lang
包,可以理解为一个内比较器,因为实现了comparable接口的类可以和自己比较,要和其他实现了Comparable接口类比较,可以使用compareTo(objectobj)
方法。compareTo方法的返回值是int,有三种情况:
-
返回正整数(比较者大于被比较者)
-
返回0(比较者等于被比较者)
-
返回负整数(比较者小于被比较者)
comparator
接口出自java.util
包,它有一个compare(object obj1,object obj2)
方法用来排序,返回值同样是int,有三种情况,和compareTo类似。
它们之间的区别:
-
很多包装类都实现了comparable接口,像Integer、string等。所以直接调用
co1lections.sort()
直接可以使用。如果对类里面自带的自然排序不满意,而又不能修改其源代码的情况下,使用comparator
就比较合适。 -
此外使用
comparator
可以避免添加额外的代码与我们的目标类耦合,同时可以定义多种排序规则,这一点是comparable接口没法做到的 -
从灵活性和扩展性讲
Comparator
更优,故在面对自定义排序的需求时,可以优先考虑使用comparator接口。
9.Collection和Collections有什么区别?
-
Collection
:是最基本的集合接口,它提供了对集合对象进行基本操作的通用接口方法。一个Collection代表一组Object,即Collection的元素。它的直接继承接口有List,Set 和Queue。 -
Collections
:是不属于Java的集合框架的,它是集合类的一个工具类。此类不能被实例化,服务于Java的Collection框架。它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。
PriorityQueue
是在 JDK1.5 中被引入的, 其与 Queue
的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。
它有这些特点:
-
PriorityQueue
利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据。 -
PriorityQueue
通过堆元素的上浮和下沉,实现了在O(logn)
的时间复杂度内插入元素和删除堆顶元素。 -
PriorityQueue
是非线程安全的,且不支持存储NULL
和non-comparable
的对象。 -
PriorityQueue
默认是小顶堆,但可以接收一个Comparator
作为构造参数,从而来自定义元素优先级的先后。 -
默认容量是
11
。当数组比较小(小于64)的时候每次扩容容量翻倍。当数组比较大(大于等于64)的时候每次扩容只增加一半的容量。 -
PriorityQueue
不是有序的,只有堆顶存储着最小的元素
可以参考PriorityQueue源码:https://blog.csdn.net/qq_45966440/article/details/122273598?spm=1001.2014.3001.5501
HashSet
的实现是依赖于HashMap
的,HashSet 的值都是存储在HashMap中的。在 HashSet 的构造法中会初始化一个HashMap对象,HashSet 不允许值重复。因此,HashSet的值是作为HashMap的key存储在HashMap 中的,当存储的值已经存在时返回false。
👨💻面试官追问:HashSet有哪些特点?
-
无序性(存储元素无序)
-
唯一性(允许使用null)本质上,HashSet的值是作为HashMap的key存储在HashMap 中的,因此保证唯一性
-
HashSet没有提供
get()
方法,同HashMap一样,因为Set内部是无序的,所以只能通过迭代的方式获得
-
JDK1.7:数组 + 链表
-
JDK1.8:数组 + (链表 | 红黑树)
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK1.8的hash方法:
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JDK1.7的hash方法:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
从源码可以看出JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
-
计算索引时效率更高:
hash % tab.length
,而计算机中直接求余运算效率不如位移运算。所以源码中做了优化,使用hash & (tab.length- 1)
来寻找桶位。而实际上hash % length
等于hash & ( length - 1)
的前提是 length 必须为 2 的 n 次幂 -
扩容时重新计算索引效率更高:
hash & oldCap == 0
的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
-
当根据 key 的 hash 值寻址计算确定桶位下标 index 时,如果HashMap 的数组长度 tab.length 是 2 的 n 次幂数,那么就可以保证新插入数组中的数据均匀分布,每个桶位都有可能分配到数据,而如果数组长度不是 2 的 n 次幂数,那么就可能导致一些桶位上永远不会被插入到数据,反而有些桶位频繁发生 hash 冲突,导致数组空间浪费,冲hash 突概率增加。
-
计算key的hash值
-
如果桶(数组)数量为0,则初始化桶
-
如果key所在的桶没有元素,则直接插入
-
如果key所在的桶中的第一个元素的key与待插入的key相同,说明找到了元素,转后续流程(9)处理
-
如果第一个元素是树节点,则调用树节点的putTreeVal()寻找元素或插入树节点
-
如果不是以上三种情况,则遍历桶对应的链表查找key是否存在于链表中
-
如果找到了对应key的元素,则转后续流程(9)处理
-
如果没找到对应key的元素,则在链表最后插入一个新节点并判断是否需要树化
-
如果找到了对应key的元素,则判断是否需要替换旧值,并直接返回旧值
-
如果插入了元素,则数量加1并判断是否需要扩容
详细的HashMap方法执行过程可以参考:【JDK源码】HashMap源码分析(附常见面试题)
-
计算key的hash值
-
找到key所在的桶及其第一个元素
-
如果第一个元素的key等于待查找的key,直接返回
-
如果第一个元素是树节点就按树的方式来查找
-
否则就按链表方式查找
-
如果都没有,返回null
-
如果使用是默认构造方法,则第一次插入元素时初始化为默认值,容量为
16
,扩容门槛为12
-
如果使用的是非默认构造方法,则第一次插入元素时初始化容量等于扩容门槛,扩容门槛在构造方法里等于传入容量向上最近的2的n次方
-
如果旧容量大于0,则新容量等于旧容量的2倍,但不超过最大容量2的30次方,新扩容门槛为旧扩容门槛的2倍
-
创建一个新容量的桶
-
搬移元素,原链表分化成两个链表,低位链表存储在原来桶的位置,高位链表搬移到原来桶的位置加旧容量的位置
关于这部分建议详细看看源码:【JDK源码】HashMap源码分析(附常见面试题)
必须满足两个条件:
-
链表长度超过树化阈值
>8
-
数组容量
>=64
当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化。
👨💻面试官追问:那什么时候树化退化?
-
情况1:在扩容时如果拆分树时,树元素个数
<= 6
则会退化链表 -
情况2:移除之前,remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
18.HashMap底层为什么选择红黑树而不用其他树,比如二叉查找树?
二叉查找树在特殊情况下也会变成一条线性结构,和原先的长链表存在一样的深度遍历问题,查找性能慢,如图:
使用红黑树主要是为了提升查找数据的速度,红黑树是平衡二叉树的一种,插入新数据(新数据初始是红色结点插入)后会通过左旋,右旋,变色等操作来保持平衡,解决单链表查询深度的问题。
👨💻面试官追问:那为什么要将链表中转红黑树的阈值设为8?
之所以以 8
为树化门槛,是因为经过大量测试,8 这个值是最合适的。理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,长度超过 8 的链表出现概率是 0.00000006
。树化阈值选择 8 就是为了让树化几率足够小
👨💻面试官继续追问:那为什么不一开始直接使用红黑树?
-
当链表数据量少的时候,遍历线性链表比遍历红黑树消耗的资源少 (因为少量数据,红黑树本身自选、变色保持平衡也是需要消耗资源的),所以前期使用线性表。
-
然后TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
19.HashMap扩容(加载)因子为何默认是 0.75f?
-
在空间占用与查询时间之间取得较好的权衡
-
大于这个值,空间节省了,但链表就会比较长影响性能
-
小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
我们先看源码:
static final int hash(Object key) {
int h;
//key==null直接返回0
//1、否则调用key的hashCode()方法计算出key的哈希值然后赋值给h,
//2、h >>> 16。后与h无符号右移16位后的二进制进行按位异或得到最后的hash值,
//3、这样做是为了使计算出的hash更分散,让高16位可以参与(低16位具有高16位的特征)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看出:
-
首先,计算对象的
hashCode()
-
然后将 key 的 hashCode 的高 16 位和 hashCode 低 16 位 进行异或(XOR)运算,最终得到新的 hash 值。二次 hash() 是为了综合高位数据,让哈希分布更为均匀
关于第二点,这里举个例子就知道了:
我们知道,HashMap 新插入的数据需要经过寻址算法 index = hash & (tab.length - 1)
来确定桶位下标。tab.length就是数组长度,我们这里设其为 n。
如果当 n 即数组长度很小,假设是 n = 16 的话,那么 n - 1 是 15 ,其二进制数为 1111 ,这样的值和 hashCode 直接做按位与操作,实际上只使用了哈希值的后 4 位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题
我们来看一个分析图:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
面试题文档来啦,内容很多,485页!
由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。
1111道Java工程师必问面试题
MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:
Elasticsearch 24 题 +Memcached + Redis 40题:
Spring 26 题+ 微服务 27题+ Linux 45题:
Java面试题合集:
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
95%以上Java开发知识点,真正体系化!**
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
面试题文档来啦,内容很多,485页!
由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。
1111道Java工程师必问面试题
[外链图片转存中…(img-Qy2KxpLC-1713312545778)]
MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:
[外链图片转存中…(img-Ptn1o4Ba-1713312545778)]
Elasticsearch 24 题 +Memcached + Redis 40题:
[外链图片转存中…(img-SpZvADPx-1713312545779)]
Spring 26 题+ 微服务 27题+ Linux 45题:
[外链图片转存中…(img-vUZgYFqT-1713312545779)]
Java面试题合集:
[外链图片转存中…(img-uqhdCcu3-1713312545779)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!