Java 集合框架面经

1、说说有哪些常见的集合框架?

集合框架可以分为两条大的支线:

  1. Map 接口:表示键值对的集合,一个键映射到一个值。键不能重复,每个键只能对应一个值。Map 接口的实现类包括 HashMap、LinkedHashMap、TreeMap 等。
  2. Collection 接口:最基本的集合框架表示方式,提供了添加、删除、清空等基本操作,它主要有三个子接口:
  • List 代表有序、可重复的集合,典型代表就是封装了动态数组的 ArrayList 和封装了链表的 LinkedList。
  • Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet。
  • Queue 代表队列,典型代表就是双端队列 ArrayDeque,以及优先级队列 PriorityQueue。

1.1 集合框架有哪几个常用工具类?

集合框架位于 java.util 包下,提供了两个常用的工具类:

  1. Collections:提供了一些对集合进行排序、二分查找、同步的静态方法。
  2. Arrays:提供了一些对数组进行排序、打印、和 List 进行转换的静态方法。

1.2 简单介绍一下队列?

Java 中的队列主要通过 Queue 接口和并发包下的 BlockingQueue 两个接口来实现。

优先级队列 PriorityQueue 实现了 Queue 接口,是一个无界队列,它的元素按照自然顺序排序或者 Comparator 比较器进行排序。双端队列 ArrayDeque 也实现了 Queue 接口,是一个基于数组的,可以在两端插入和删除元素的队列。

1.3 用过哪些集合类,它们的优劣?

我常用的集合类有 ArrayList、LinkedList、HashMap、LinkedHashMap。

ArrayList 可以看作是一个动态数组,可以在需要时动态扩容数组的容量,只不过需要复制元素到新的数组。优点是访问速度快,可以通过索引直接查找到元素。缺点是插入和删除元素可能需要移动或者复制元素。

LinkedList 是一个双向链表,适合频繁的插入和删除操作。优点是插入和删除元素的时候只需要改变节点的前后指针,缺点是访问元素时需要遍历链表。

HashMap 是一个基于哈希表的键值对集合。优点是可以根据键的哈希值快速查找到值,但有可能会发生哈希冲突,并且不保留键值对的插入顺序。

LinkedHashMap 在 HashMap 的基础上增加了一个双向链表来保持键值对的插入顺序。

1.4 队列和栈的区别了解吗?

队列是一种先进先出的数据结构,第一个加入队列的元素会成为第一个被移除的元素。栈是一种后进先出的数据结构,最后一个加入栈的元素会成为第一个被移除的元素。

1.5 哪些是线程安全的容器?

Vector、Hashtable、ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、ArrayBlockingQueue、LinkedBlockingQueue 都是线程安全的。

1.6 Collection 继承了哪些接口?

Collection 继承了 Iterable 接口,这意味着所有实现 Collection 接口的类都必须实现 iterator() 方法,之后就可以使用增强型 for 循环遍历集合中的元素了。

2、ArrayList 和 LinkedList 有什么区别?

ArrayList 是基于数组实现的,LinkedList 是基于链表实现的。

2.1 ArrayList 和 LinkedList 的用途有什么不同?

多数情况下,ArrayList 更利于改查,LinkedList 更利于增删。

2.2 ArrayList 和 LinkedList 是否支持随机访问?

  1. ArrayList 是基于数组的,也实现了 RandomAccess 接口,所以它支持随机访问,可以通过下标直接获取元素。
  2. LinkedList 是基于链表的,所以它没法根据下标直接获取元素,不支持随机访问。

2.3 ArrayList 和 LinkedList 内存占用有何不同?

  1. ArrayList 是基于数组的,是一块连续的内存空间,所以它的内存占用是比较紧凑的;但如果涉及到扩容,就会重新分配内存,空间是原来的 1.5 倍。
  2. LinkedList 是基于链表的,每个节点都有一个指向下一个节点和上一个节点的引用,于是每个节点占用的内存空间比 ArrayList 稍微大一点。

2.4 ArrayList 和 LinkedList 的使用场景有什么不同?

ArrayList 适用的场景:

  1. 随机访问频繁:需要频繁通过索引访问元素的场景。
  2. 读取操作远多于写入操作:如存储不经常改变的列表。
  3. 末尾添加元素:需要频繁在列表末尾添加元素的场景。

LinkedList 适用的场景:

  1. 不需要快速随机访问:顺序访问多于随机访问的场景。
  2. 频繁插入和删除:在列表中间频繁插入和删除元素的场景。
  3. 队列和栈:由于其双向链表的特性,LinkedList 可以实现队列(FIFO)和栈(LIFO)。

2.5 链表和数组有什么区别?

  1. 数组在内存中占用的是一块连续的存储空间,因此我们可以通过数组下标快速访问任意元素。数组在创建时必须指定大小,一旦分配内存,数组的大小就固定了。
  2. 链表的元素存储在于内存中的任意位置,每个节点通过指针指向下一个节点。

3、ArrayList 的扩容机制了解吗?

当往 ArrayList 中添加元素时,会先检查是否需要扩容,如果当前容量 + 1 超过数组长度,就会进行扩容。扩容后的新数组长度是原来的 1.5 倍,然后再把原数组的值拷贝到新数组中。

4、ArrayList 怎么序列化的知道吗?

ArrayList 自定义了序列化逻辑从而只序列化有效数据,因为数组的容量一般大于实际的元素数量。

4.1 为什么 ArrayList 不直接序列化元素数组呢?

出于效率的考虑,数组可能长度 100,但实际只用了 50,剩下的 50 没用到,也就不需要序列化。

5、快速失败 fail-fast 了解吗?

在用迭代器遍历集合对象时,如果线程 A 遍历过程中,线程 B 对集合对象的内容进行了修改,就会抛出 Concurrent Modification Exception。迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如 ArrayList 类。

5.1 什么是安全失败?

采用安全失败机制的集合容器,在遍历时并不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如 CopyOnWriteArrayList 类

6、有哪几种实现 ArrayList 线程安全的方法?

  1. 可以使用 Collections.synchronizedList() 方法,它可以返回一个线程安全的 List。内部是通过 synchronized 关键字加锁来实现的。
  2. 也可以直接使用 CopyOnWriteArrayList,它是线程安全的 ArrayList,遵循写时复制的原则,每当对列表进行修改时,都会创建一个新副本,这个新副本会替换旧的列表,而对旧列表的所有读取操作仍然在原有的列表上进行。

6.1 ArrayList 和 Vector 的区别?

Vector 属于 JDK 1.0 时期的遗留类,不推荐使用,仍然保留着是因为 Java 希望向后兼容。

ArrayList 是在 JDK 1.2 时引入的,用于替代 Vector 作为主要的非同步动态数组实现。因为 Vector 所有的方法都使用了 synchronized 关键字进行同步,所以单线程环境下效率较低。

7、CopyOnWriteArrayList 了解多少?

CopyOnWriteArrayList 就是线程安全版本的 ArrayList。采用了一种读写分离的并发策略。CopyOnWriteArrayList 容器允许并发读,读操作是无锁的。至于写操作,比如说向容器中添加一个元素,首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

8、能说一下 HashMap 的底层数据结构吗?

JDK 8 中 HashMap 的数据结构是数组 + 链表 + 红黑树:

数组用来存储键值对,每个键值对可以通过索引直接拿到,索引是通过对键的哈希值进行进一步的 hash() 处理得到的。当多个键经过哈希处理后得到相同的索引时,需要通过链表来解决哈希冲突:将具有相同索引的键值对通过链表存储起来。不过,链表过长时,查询效率会比较低,于是当链表的长度超过 8 时(且数组的长度大于 64),链表就会转换为红黑树。红黑树的查询效率是 O(logn),比链表的 O(n) 要快。

hash() 方法的目标是尽量减少哈希冲突,保证元素能够均匀地分布在数组的每个位置上。如果键的哈希值已经在数组中存在,其对应的值将被新值覆盖。

HashMap 的初始容量是 16,随着元素的不断添加,HashMap 就需要进行扩容,阈值是capacity * loadFactor,capacity 为容量,loadFactor 为负载因子,默认为 0.75。扩容后的数组大小是原来的 2 倍,然后把原来的元素重新计算哈希值,放到新的数组中。

9、你对红黑树了解多少?

红黑树是一种自平衡的二叉查找树:

  1. 左根右;
  2. 根叶黑;
  3. 不红红;
  4. 黑路同;

9.1 为什么不用二叉树?

二叉树是最基本的树结构,每个节点最多有两个子节点,但是二叉树容易出现极端情况,比如插入的数据是有序的,那么二叉树就会退化成链表,查询效率就会变成 O(n)。

9.2 为什么不用平衡二叉树?

平衡二叉树比红黑树的要求更高,每个节点的左右子树的高度最多相差 1,这种高度的平衡保证了极佳的查找效率,但在进行插入和删除操作时,可能需要频繁地进行旋转来维持树的平衡,维护成本更高。

9.3 为什么用红黑树?

链表的查找时间复杂度是 O(n),当链表长度较长时,查找性能会下降。红黑树是一种折中的方案,查找、插入、删除的时间复杂度都是 O(log n)。

9.4 红黑树插入删除规则?

​默认新节点为红色:

​插入逻辑:若插入后破坏红黑性质:

​情况1:父节点为黑色 → 无需调整。

​情况2:父节点为红色,递归调整。

删除逻辑:更复杂,需处理黑节点删除后的黑高减少问题。

10、红黑树怎么保持平衡的?

旋转和染色:

  1. 旋转:通过左旋和右旋来调整树的结构,避免某一侧过深。
  2. 染⾊:修复红黑规则,从而保证树的高度不会失衡。

11、HashMap 的 put 流程知道吗?

  1. 首先计算键的哈希值,通过 hashCode() 高位运算和扰动函数,减少哈希冲突。
  2. 进行第一次的数组扩容,确定桶的位置,根据哈希值按位计算数组索引。
  3. 处理键值对,若桶为空,直接插入新节点。若桶不为空,判断当前位置第一个节点是否与新节点的 key 相同,如果相同则更新 value,如果不同说明发生哈希冲突,吐过是链表,将新节点添加到链表的尾部;如果链表长度大于等于 8,则将链表转换为红黑树。
  4. 检查容量并扩容,插入后若 size > threshold (capacity * loadFactor),则扩容为原容量的两倍。并且重新计算每个节点的索引,数据将会重新分布。

11.1 只重写元素的 equals 方法没重写 hashCode,put 的时候会发生什么?

如果只重写 equals 方法,没有重写 hashCode 方法,那么会导致 equals 相等的两个对象,hashCode 不相等,这样的话,两个对象会被 put 到数组中不同的位置,size + 1,导致大量哈希冲突,退化成链表,查询效率降为 O(n)。

12、待更

13、

14、

15、

16、

17、

18、

相关推荐
孤客网络科技工作室2 分钟前
每天学一个 Linux 命令(7):cd
java·linux·前端
nqqcat~6 分钟前
STL常用算法
开发语言·c++·算法
快乐非自愿11 分钟前
Netty源码—10.Netty工具之时间轮
java·unity·.net
快来卷java19 分钟前
常见集合篇(二)数组、ArrayList与链表:原理、源码及业务场景深度解析
java·数据结构·链表·maven
Matlab光学30 分钟前
MATLAB仿真:Ince-Gaussian光束和Ince-Gaussian矢量光束
开发语言·算法·matlab
ktkiko1136 分钟前
用户模块——整合 Spring 缓存(Cacheable)
java·spring·缓存
珹洺1 小时前
Java-servlet(十)使用过滤器,请求调度程序和Servlet线程(附带图谱表格更好对比理解)
java·开发语言·前端·hive·hadoop·servlet·html
勘察加熊人1 小时前
c#使用forms实现helloworld和login登录
开发语言·c#
上等猿1 小时前
Elasticsearch笔记
java·笔记·elasticsearch
双叶8361 小时前
(C语言)学生信息表(学生管理系统)(基于通讯录改版)(正式版)(C语言项目)
c语言·开发语言·c++·算法·microsoft