最后
针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。
下面的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图)
最新整理电子书
| 数据结构和算法 | 传送门 |
| 数据库 | 传送门 |
| JavaWeb | 传送门 |
| 设计模式 | 传送门 |
| Spring、MyBatis | 传送门 |
目录:
-
前言
-
1 Java 集合概论
-
2 如何选用集合
-
3 扩容机制
-
- 3.1 ArrayList 的扩容机制
-
3.2 LinkedList 的扩容机制
-
3.3 HashMap 的扩容机制
-
4 HashMap 的相关问题?
-
- 4.1 HashMap 的底层实现
-
4.2 为什么要改成“数组+链表+红黑树”
-
4.3 面试回答
-
5 HashSet 的相关问题
-
- 5.1 HashSet 的底层实现
-
5.2 HashSet 如何检查重复
-
6 ConcurrentHashMap 的相关问题
-
- 6.1 ConcurrentHashMap 的底层具体实现
-
6.2 面试回答
-
7 相似集合的异同
-
- 7.1 ArrayList 和 LinkedList 的异同
-
7.2 ArrayList 和 Vector 的异同
-
7.3 HashMap 和 Hashtable 的异同
-
7.4 HashMap 和 ConcurrentHashMap 的异同
-
7.5 ConcurrentHashMap 和 Hashtable 的异同
-
7.6 HashMap 和 HashSet 的异同
-
7.7 TreeSet 和 HashSet 的异同
==============================================================================
数组(Array)和集合都是对多个数据进行存储操作的结构,也简称为 Java 容器。此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(文件或数据库)。
然而目前数组(Array)在存储数据方面存在以下特点:
-
数组初始化以后,长度就不可变了,不便于扩展;
-
数组声明的类型,就决定了进行元素初始化时的类型;
-
数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,同时无法直接获取存储元素的个数;
-
数组存储的数据是有序的、可以重复的。 存储数据的特点单一;
当然,上述有些特点也可以被当作优点,然而在实际操作中,由于数组的不够灵活,所以我们需要其它的方式来存储数据,Java 集合类就这样应运而生了。
Java 集合类中有可以用于存储数量不确定的多个对象的集合(Collection 接口),还有可用于保存具有映射关系的关联数组的集合(Map 接口)。
-
Collection 接口:单列数据,用于存储数量不确定的多个对象 ;
-
List接口:元素有序、可重复的集合(对应“动态”数组),即集合中的每个元素都有其对应的顺序索引,可以根据序号存取容器中的元素。
-
ArrayList 实现类:作为 List 接口的主要实现类,它是线程不安全的,但效率高;适⽤于频繁的查找⼯作。
-
LinkedList 实现类:使用 LinkedList 实现类对于频繁的插入、删除操作的效率比 ArrayList 高;
-
Vector 实现类:作为 List 接口的古老实现类,它是线程安全的,但效率低;
-
Set 接口:元素无序、不可重复的集合(对应高中讲的“集合”)
-
HashSet 实现类:作为 Set 接口的主要实现类,是线程不安全的;可以存储 null 值。
-
LinkedHashSet 实现类:是 HashSet 的子类。遍历其内部数据时,可以按照添加的顺序遍历,使其看起来是有序的。
-
TreeSet 实现类:底层使⽤红⿊树,TreeSet可以确保集合元素处于排序状态;排序方法分为自然排序和定制排序,自然排序指的是根据自然规律排序,比如数字的话就按照从小到大排序,定制排序指的是根据自己既定的规则进行排序。
-
Map 接口:双列数据,保存具有映射关系 “key-value对” 的集合
-
HashMap 实现类:作为Map的主要实现类,它是线程不安全的,但效率高。可以存储 null 值的 key 和 value。
-
LinkedHashMap 实现类:保证在遍历 map 元素时,可以按照添加的顺序实现遍历。
-
ConcurrentHashMap 实现类:线程安全的 Map;
-
TreeMap 实现类:底层使用红黑树,保证按照添加的 key-value 对进行排序,实现排序遍历。
-
Hashtable 实现类:古老的实现类,它是线程安全的,但效率低;不能存储 null 的 key 和 value。
-
Properties 实现类:常用来处理配置文件,key 和 value 都是 String 类型。
Java中的 List、Set、Map 之类的集合容器中只能存放引用类型,而不能存放类似于 int、double 之类的基本类型。
参考文献:【传送门】
===========================================================================
如果我们需要根据键值获取元素值就选用 Map 接口下的集合,当需要排序时就选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap;
如果我们只需要存放元素时,就选择实现 Collection 接口的集合,此时如果需要保证元素唯一就选择实现 Set 接口的集合,如 TreeSet 或者 HashSet,此时如果需要排序就只选择 TreeSet;
当确定选择实现 Collection 接口的集合后,如果不需要保证元素唯一,就选择实现 List 接⼝的 ArrayList 或 LinkedList 等集合,此时如果需要线程安全的话,就只能选择 Vector,当不需要保证线程安全时,该集合需要频繁的删除和插入,就选择 LinkedList,需要频繁的查找就选择 ArrayList;
=========================================================================
ArrayList 底层的数据结构是动态数组,其扩容情况可以分为两种情况:
第一种情况,当 ArrayList 的容量为 0 时,此时添加元素的话,需要扩容,三种构造方法创建的 ArrayList 在扩容时略有不同:
-
无参构造,创建 ArrayList 后容量为 0,添加第一个元素后,容量变为 10,此后若需要扩容,则按照 1.5 倍进行扩容;
-
当传容量,参数为 0 或者传列表,列表为空构造时,创建 ArrayList 后容量为 0,添加第一个元素后,容量为 1,此时 ArrayList 是满的,下次添加元素时则按照 1.5 倍进行扩容;
第二种情况,当 ArrayList 的容量大于 0,并且 ArrayList 是满的时,此时添加元素的话,进行正常扩容,每次扩容到原来的 1.5 倍。
ArrayList 没有缩容。无论是remove
方法还是clear
方法,它们都不会改变现有数组elementData
的长度。但是它们都会把相应位置的元素设置为null
,以便垃圾收集器回收掉不使用的元素,节省内存。
LinkedList 的底层是双向链表,所以不需要主动扩容。ArrayList 的底层是动态数组,所以当数组的内存不足时,需要主动扩容。
HashMap 的扩容机制分为两种情况:
第一种情况,无参构造时,HashMap 的默认初始容量是16,之后每次扩充,容量变为原来的 2 倍;
第二种情况,有参构造时,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的 2 的 N 次方,例如传 9,容量为16。之后每次扩充,容量变为原来的 2 倍。
为什么HashMap 的容量大小永远是 2 的 N 次方呢?
**为了能让 HashMap 存取⾼效,尽量减少哈希碰撞,也就是要尽量把数据分配均匀。**我们上⾯也讲过了,Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。
我们⾸先可能会想到采⽤ % 取余的操作来实现。但是,重点来了:“如果除数是2的幂次,那么取余(%)操作则等价于与其被除数减⼀的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于 % 能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是 2 的幂次⽅。
===================================================================================
JDK1.8 之前:
此时 HashMap 的底层实现是数组+链表的形式,HashMap 通过 key 的 hashCode() 经过扰动函数处理过后得到 hash 值,然后利用公式((n - 1) & hash)判断当前元素存放的位置,如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash 值以及 key 值是否相同,如果均相同则直接覆盖,如果值不同,则通过拉链法来解决哈希冲突。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 及之后:
对于插入,相比于之前的版本, JDK1.8 在解决哈希冲突时有了较⼤的变化,当链表长度大于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。
对于移除,当同一个索引位置的节点在移除后小于 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。
主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n)(线性级),而使用红黑树是 O(logn)(对数级)。
请问 HashMap 的底层实现是怎么样的?
在 JDK1.8 之前的话,HashMap 的底层是通过数组+链表的形式来实现的,在 JDK1.8 及之后的话,则是通过数组+链表/红黑树的形式来实现,这两者的区别在于,当哈希冲突严重时,后者会将链表转换成红黑树来存储,从而提升查找性能。
2 什么是哈希冲突
因为 HashMap 在存储数据时是根据键的哈希值所对应的位置来存放的,然后可能存在键不同,对应的位置相同的情况,这种情况便是哈希冲突,在实际中,常采用拉链法来解决,拉链法指的是将链表和数组相结合,数组中每⼀格就是⼀个链表,若遇到哈希冲突,则将冲突的值加到链表后即可。
3 什么是红黑树,红黑树如何进行自调整,与哈希表的区别
红黑树是一种可以自平衡的二叉查找树。(两头乌,路径上不能有两个连续的红色节点)
红黑树通过变色、左旋转和右旋转来保持平衡。
与哈希表的区别:
-
在增改查性能上,红黑树的性能是稳定且高效的,而哈希表当哈希碰撞严重时,时间复杂度会达到线性级;
-
红黑树是有序的,哈希表是无序的;
-
相对来说,红黑树占用的内存更小。
==================================================================================
HashSet 的底层实际上是通过 HashMap 来实现的,除了实现 Set 接口所规范的方法外,其它方法都是直接调用 HashMap,HashSet 将对应元素存放在 HashMap 的键上,与此相对应的值存放了一个静态的 final 修饰的 Object 对象。
读者福利
更多笔记分享
都是直接调用 HashMap,HashSet 将对应元素存放在 HashMap 的键上,与此相对应的值存放了一个静态的 final 修饰的 Object 对象。
读者福利
[外链图片转存中…(img-gdsDPvmX-1714863399497)]
更多笔记分享
[外链图片转存中…(img-dW4zKaD4-1714863399497)]