🧸本篇博客重在讲解Java集合与数据结构面试题,将会实时更新,欢迎大家添加作者文末联系方式交流
📜JAVA面试题专栏:JAVA崭新面试题——2024版_dream_ready的博客-CSDN博客
📜作者首页: dream_ready-CSDN博客
📜有任何问题都可以评论留言,作者将会实时回复
📜未来将会更新数据结构面试题以及Spring和MySQL等等的面试题,将会涵盖JAVA后端所有技术框架,敬请期待!!!
注:本篇博客题目顺序是精心制作的,建议想要阅读完整篇博客的同学按着顺序阅读!
目录
10、为什么Jdk8之后(包含Jdk8),HashMap要使用红黑树,为什么不继续使用链表?
25、HashMap、Hashtable和ConcurrentHashMap的区别?
26、为什么 HashMap 允许插入 null键 和 null 值?
28、为什么Hashtable和ConcurrentHashMap不允许插入nuI键和nuI值?
30、ConcurrentHashMap是如何保证线程安全的?
1、Java的集合类有哪些?
在Java中,常用的集合主要分为五个大类,分别是列表(List)、集合(Set)、队列(Queue)、栈(Stack)、字典(Map)。
其中,前四种结构都是单一元素的集合(每个位置放一个元素),而Map则是以Key-Value键值对的形式使用的
- 列表(List):有序集合,可以包含重复元素。常见实现类有 ArrayList、LinkedList、Vector 等。
- 集合(Set):无序集合,不允许包含重复元素。常见实现类有 HashSet、TreeSet 等。
- 队列(Queue)::先进先出的数据结构。常见实现类有 BlockingQueue、PriorityQueue 等.
- 栈(Stack):先进后出的数据结构。
- 字典(Map):键值对的集合,每个键只能对应一个值。常见实现类有 HashMap、TreeMap、LinkedHashMap 等。
各个集合的特点在上面用加粗字号显示了,依此口述即可
从继承关系上讲,List,Set,Queue都是Collection的子接口,Map属于一个单独的分类
可以了解一下:Collection又继承了Iterable接口,所以我们经常可以看到,集合类使用迭代器进行遍历
口述话:(阅读即可,不要背,太口语化了)
在大部分的实际使用中,最常用的集合接口包括 List、Set、Map 等。
其中,List 接口表示一个有序的集合,可以包含重复的元素,最常用的实现类有ArrayList,可以说在代码中,只要用到集合类,它一定是脑子中最先想到的。当然,既然您都提问我八股文了,那么理论上最常用的也有LinkedList(链表),毕竟增删的话,链表确实比偏数组的ArrayList更快。其次,需要注意的是,栈,stack也是List的子接口,但是队列,Queue不是
Set 接口表示一个不重复的集合,只有当我们对“不重复”三个字有需求时才会用到它,否则还是优先使用list
Map 接口表示一组键值对的映射关系。这个不比list用的少,甚至无处不在。毕竟其他的数据结构都是一个萝卜一个坑,只有他是键值对存储的,他的查找、插入和删除都是非常迅速的,我们也需要用到它键值对的特性。我举一个常见的使用该场景吧,用户的会话管理,这个时候,我们就可以使用Map来存储用户的会话信息,也就是Session,每个用户的会话信息可以通过唯一的会话ID来索引,说白了,就是这样的结构:<key, Value>: <会话ID, Session>
2、集合和数组的区别?
与其说是集合和数组的区别,不如说是ArrayList和数组的区别
在 Java 中,数组属于一种特殊的数据结构。它是一种固定大小的、用于存储相同类型的多个元素的一种容器。数组在内存中是一个连续的存储区域,可以通过索引来访问其中的某个元素。
int[] numbers = {5, 3, 7, 2, 1};
而集合,是一种用于存储和操作一组对象的数据结构。它们通过一组接口和类来实现,提供了丰富的方法和功能使得对数据的处理更加方便和高效。
List<Integer> numbers = new ArrayList<>();
从严格意义上来讲,数组和集合都是用于存储数据的容器,但数组不属于集合。其实在我看来,二者在实际使用上的最大区别就两点
- 数组无法存储泛型,这就导致数组能存储的数据结构很有限,无法存储我们自己的实体类
- 数组在初始化时需要指定大小,后续改变存储大小就很困难了,而集合我们不需要考虑这点,它会动态扩展
注:其他list的实现类和数组差别太大了,比较意义不大,咱很多时候,说集合和数组的区别,其实默认就是比较ArrayList和数组的区别
3、ArrayList和LinkedList的区别
说白了,更像是数组和链表的区别,ArrayList是一个可以改变大小的数组,LinkedList是一个双向链表
注:为了防止有些同学困惑,我还是在这里说一下吧,严格意义上,ArrayList是集合,数组是类似int[]这样的,你要专门比较二者的话,你肯定不能说ArrayList是数组,但在我们不讨论严格意义上的数组时,我们是经常会把ArrayList说成数组的,这也是所有开发者的习惯了
ArrayList和LinkedList都是数据的集合,区别如下:
1、底层数据结构实现不同:
- ArrayList底层使用数组实现,是一块连续的内存区域,通过一个可调整大小的数组来存储元素
- LinkedList 底层使用双向链表实现,可以由不连续的内存区域实现,它通过链表节点来连接元素,每个元素都会存储上一个元素和下一个元素的地址
2、插入和删除的效率不同:
- ArrayList 对于插入和删除操作的性能相对较低,因为需要进行元素的移动和数组的重新分配,尤其是在ArrayList 列表最前面插入和删除时,效率最慢。
- LinkedList 对于插入和删除操作会比 ArrayList 更好,因为它只需要修改相邻节点的指针即可。
3、随机访问效率不同:
- ArrayList 对于随机访问(根据索引获取元素)具有更好的性能,因为可以通过索引直接计算元素在数组中的位置,时间复杂度为 O(1)
- LinkedList 对于随机访问的性能较差,需要通过链表节点一个个遍历找到对应的索引位置,时间复杂度为O(n)
4、内存要求和占用空间大小不同:
- ArrayList 在内存中需要连续的存储空间,因此在存储大量数据时,需要有大块的连续内存空间,所以对内存要求较高(不能有太多的内存碎片)。
- LinkedList 不要求有连续的内存空间,它的链表是逻辑的先后顺序,每个元素用额外的空间来存储指向前和后的节点指针,所以,LinkedList 相对而言会占用更多的内存空间。
因此,在多查的场景下考虑使用 ArrayList,而在插入和删除比较多的场景下考虑使用 LinkedList
4、ArrayList和Vector有什么区别?
ArrayList 和 Vector 都是 List 的子类,都实现了 List 接口,如下图所示:
首先,需要明白的是,尽管从上图看,Stack继承Vector,但是Vector已经不是接口了,是一个具体的类
二者的区别主要有以下几点:
- 线程安全不同:Vector 是线程安全的,而 ArrayList 是非线程安全的。在 Vector中,每个方法都使用了synchronized 关键字进行同步,以确保线程安全。
- 扩容策略不同:Vector 和 ArrayList 都是可以动态扩容的。但是,当动态的长度不同,Vector 会自动增长当前的容量一倍,也就是扩容 100%,而 ArrayList 只会增长当前容量的 50%。
- 性能不同:ArrayList 由于不具备线程安全的机制,所以在单线程环境下通常有更好的性能。而 Vector 由于是线程安全的,使用 synchronized 修饰,所以会带来额外的加锁和释放锁的性能开销,因此在单线程环境下Vector 性能不如 ArrayList。
5、ArrayList是如何扩容的?
首先,我们要明白ArrayList是基于数组的,我们都知道,申请数组的时候,只能申请一个定长的数组,那么List是如何通过数组扩容的呢?
ArrayList都是在新增元素时发现容量不足而触发扩容操作的
ArrayList的扩容分为以下几步:
- 检查新增元素后是否会超过数组的容量,如果超过,则进行下一步扩容
- 设置新的容量为老容量的1.5倍,容量最多不超过2^31-1
- 之后,申请一个容量为1.5倍的数组,并将元素复制到新数组中,扩容完成。
- 最后将原本要添加的那个新元素添加到数组中
6、ArrayList是线程安全的么
不是!
多线程环境下,如果多个线程同时对同一个 ArrayList 进行添加、删除或修改操作,可能会导致数据不一致或发生异常。这是因为,ArrayList 在内部实现时,并没有添加任何线程同步的机制,所以如果有多个线程同时对ArrayList 进行修改时,就会导致线程不安全的问题发生。
例如,添加操作和修改操作同时执行,那么它们的执行情况可能是这样的:
- 首先,先执行添加操作,而添加时发现 ArrayList 需要进行扩容,所以此时就先执行了扩容操作。
- 扩容操作执行一半之后,当前线程 CPU 时间片用完了,停止执行。
- 修改线程开始执行,于是将 ArrayList 已经扩容的这部分旧数据进行修改,修改线程执行完成。(问题已经出现了,因为修改的是原数组,也就是老数组)
- 扩容操作继续执行,将后半一半数组进行扩容。
- 将原对象的引用更换到新数组上。
此时就会发现,修改线程的修改操作失效了,因为修改线程,修改的是老数组,而添加操作在扩容时,已经将旧数据复制到新数组了,所以此时的修改操作就丢失了,这就是线程安全问题。
7、如何让 ArrayList 变成线程安全的?
想要让 ArrayList 变成线程安全的,也就是想要在多线程下使用 ArrayList 的方案有以下两类
- 加锁:在多线程下,对 ArrayList 进行非查询操作时,先加锁,可以使用 synchronized 或 Lock,让线程排队执行,这样对于 ArrayList 的操作就变成单线程了,这样 ArrayList 就是线程安全的了。
- 更换同类型线程安全的容器:在多线程下可以将 ArrayList 更换为线程安全的 CopyOnWriteArrayList或Vector,这样也不会有线程安全问题了。
这里简单说一下,CopyOnWriteArrayList和Vector分别靠什么实现线程安全的(防止被面试官提问)
- CopyOnWriteArrayList是使用 “写时复制” 的机制来实现线程安全的。具体来说,每当有写操作(如add、remove)发生时,CopyOnWriteArrayList 会创建一个新的数组副本,并在新