Java集合常见面试题总结(上)
背诵重点
上篇重点放在集合框架、List、Set 和 Queue。回答时不要展开太多源码,先讲清楚“数据结构、是否有序、是否允许重复、是否线程安全、适用场景”。
建议按这条线背:
- 集合体系:
Collection和Map的区别。 - List:
ArrayList、LinkedList、Vector。 - Set:
HashSet、LinkedHashSet、TreeSet。 - Queue:
Queue、Deque、PriorityQueue、BlockingQueue。 - 常见机制:扩容、fail-fast、比较器、阻塞队列。
Java 集合框架有哪些主要接口?
Java 集合主要分两大类:
Collection:存放单个元素,常见子接口有List、Set、Queue。Map:存放键值对,常见实现有HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。
Collection 关注“一个个元素怎么存”,Map 关注“key 到 value 的映射关系”。
List、Set、Queue、Map 有什么区别?
List:元素有序、可重复,支持按下标访问,常见实现是 ArrayList、LinkedList。
Set:元素不可重复,常见实现是 HashSet、LinkedHashSet、TreeSet。
Queue:队列结构,通常按先进先出或优先级规则处理元素,常见实现是 ArrayDeque、PriorityQueue、各种阻塞队列。
Map:键值对结构,key 不可重复,value 可以重复,常见实现是 HashMap、TreeMap、ConcurrentHashMap。
一句话:List 管顺序,Set 管唯一,Queue 管排队,Map 管映射。
集合框架底层数据结构怎么记?
常见集合底层结构:
ArrayList:动态数组。LinkedList:双向链表。HashSet:底层基于HashMap。LinkedHashSet:底层基于LinkedHashMap。TreeSet:红黑树。HashMap:数组 + 链表 + 红黑树。LinkedHashMap:HashMap+ 双向链表。TreeMap:红黑树。PriorityQueue:堆。ArrayDeque:循环数组。
如何选用集合?
按需求选:
- 需要按下标快速访问:
ArrayList。 - 需要频繁头尾操作:
ArrayDeque或LinkedList。 - 需要元素唯一:
HashSet。 - 需要元素唯一且保持插入顺序:
LinkedHashSet。 - 需要元素唯一且排序:
TreeSet。 - 需要 key-value 存储:
HashMap。 - 需要 key 有序:
TreeMap。 - 需要线程安全的 Map:
ConcurrentHashMap。 - 需要生产者消费者模型:
BlockingQueue。
为什么要使用集合?
数组长度固定,扩容、删除、查找都需要自己处理。集合封装了动态扩容、泛型、迭代、排序、去重、映射等能力,使用更灵活。
数组适合固定长度、简单访问;集合适合业务中数量不确定、操作复杂的数据。
ArrayList 和数组有什么区别?
数组长度固定,创建后不能改变;ArrayList 底层也是数组,但可以自动扩容。
数组可以存基本类型和对象;ArrayList 只能存对象,基本类型需要装箱。
数组 API 简单;ArrayList 提供了 add()、remove()、contains()、iterator() 等常用操作。
一句话:数组更底层、更固定;ArrayList 更灵活、更适合业务开发。
ArrayList 和 Vector 有什么区别?
ArrayList 线程不安全,性能更好,是现在最常用的 List 实现。
Vector 方法基本加了 synchronized,线程安全但性能较差,属于早期集合类,现在很少使用。
如果需要线程安全 List,优先考虑 CopyOnWriteArrayList、Collections.synchronizedList() 或外部加锁,而不是 Vector。
Vector 和 Stack 有什么区别?
Vector 是动态数组实现的 List。
Stack 继承自 Vector,表示后进先出的栈。
现在不推荐使用 Stack,如果需要栈结构,优先使用 ArrayDeque。
ArrayList 可以添加 null 吗?
可以。ArrayList 允许存储 null。
但业务代码里不建议随意放 null,否则遍历、调用方法、拆箱时都容易出现 NullPointerException。
ArrayList 插入和删除的时间复杂度是多少?
尾部添加:通常是 O(1),如果触发扩容则是 O(n)。
头部或中间插入:O(n),因为需要移动元素。
尾部删除:O(1)。
头部或中间删除:O(n),因为需要移动元素。
总结:ArrayList 查询快,按下标访问是 O(1);中间插入删除慢,因为数组元素要移动。
LinkedList 插入和删除的时间复杂度是多少?
头尾插入/删除:O(1),因为只需要改指针。
中间插入/删除:O(n),因为要先遍历找到目标节点。
按下标访问:O(n),因为链表不能像数组一样直接定位。
面试注意:不要简单说 LinkedList 增删一定快。只有头尾操作快,中间操作仍然要遍历。
LinkedList 为什么不能实现 RandomAccess?
RandomAccess 是标记接口,表示支持快速随机访问。
ArrayList 底层是数组,可以通过下标直接定位元素,所以实现了 RandomAccess。
LinkedList 底层是链表,按下标访问需要从头或尾遍历,时间复杂度是 O(n),所以不适合实现 RandomAccess。
ArrayList 和 LinkedList 有什么区别?
核心区别:
ArrayList底层是动态数组,LinkedList底层是双向链表。ArrayList随机访问快,get(index)是 O(1)。LinkedList随机访问慢,get(index)是 O(n)。ArrayList中间插入删除慢,需要移动元素。LinkedList头尾插入删除快,中间插入删除仍需要先遍历。ArrayList内存更紧凑,LinkedList每个节点还要保存前后指针,额外开销更大。
实际开发中,大多数场景优先使用 ArrayList。如果是队列、栈、双端队列场景,通常优先使用 ArrayDeque。
ArrayList 的扩容机制是什么?
ArrayList 底层是数组。默认无参构造创建时数组还没有真正分配容量,第一次添加元素时才会初始化容量。
添加元素时如果容量不够,就会扩容。JDK 8 中通常扩容为原容量的 1.5 倍,然后把旧数组元素复制到新数组。
扩容成本是 O(n),所以如果能预估元素数量,建议创建时指定初始容量,减少扩容次数。
集合中的 fail-fast 和 fail-safe 是什么?
fail-fast:快速失败。遍历集合时,如果集合结构被其他操作修改,迭代器会尽快抛出 ConcurrentModificationException。典型集合有 ArrayList、HashMap。
fail-safe:安全失败。遍历时基于副本或弱一致机制,不会因为并发修改立刻抛异常。典型集合有 CopyOnWriteArrayList、ConcurrentHashMap。
注意:fail-fast 不是并发安全保证,只是一种尽早发现错误的机制。
遍历集合时如何安全删除元素?
不要在增强 for 循环里直接调用集合的 remove()。
安全方式:
- 使用
Iterator遍历,并调用iterator.remove()。 - 使用
removeIf()。 - 并发场景使用合适的并发集合,并理解其弱一致语义。
Comparable 和 Comparator 有什么区别?
Comparable 是自然排序接口,类自己实现 compareTo(),表示“我自己怎么比较”。
Comparator 是外部比较器,单独定义比较规则,表示“别人指定我怎么比较”。
区别:
Comparable写在类内部,排序规则通常固定。Comparator写在类外部,排序规则更灵活。- 一个类只能有一种自然排序,但可以有多个
Comparator。
HashSet、LinkedHashSet、TreeSet 有什么区别?
HashSet:无序、去重,底层基于 HashMap,查询效率通常最高。
LinkedHashSet:去重,并维护插入顺序,底层基于 LinkedHashMap。
TreeSet:去重,并按自然顺序或比较器排序,底层红黑树,增删查复杂度 O(log n)。
选择:
- 只要求去重:
HashSet。 - 去重且保留插入顺序:
LinkedHashSet。 - 去重且排序:
TreeSet。
Set 的无序性和不可重复性怎么理解?
不可重复:不能同时存放两个相等的元素。HashSet 依赖 hashCode() 和 equals() 判断重复;TreeSet 依赖比较规则判断重复。
无序:不是按添加顺序保存。注意,无序不等于随机,只是不能依赖它的遍历顺序。
如果需要保持插入顺序,用 LinkedHashSet;如果需要排序,用 TreeSet。
Queue 和 Deque 有什么区别?
Queue 是单端队列,通常从队尾入队、队头出队。
Deque 是双端队列,两端都可以插入和删除,既可以当队列,也可以当栈。
实际开发中,如果需要栈或双端队列,优先使用 ArrayDeque。
ArrayDeque 和 LinkedList 有什么区别?
两者都可以作为队列或双端队列使用。
ArrayDeque 底层是可扩容循环数组,内存连续,缓存友好,通常性能更好,不允许存 null。
LinkedList 底层是双向链表,每个节点有额外指针开销,随机访问慢,允许存 null。
实际作为栈、队列、双端队列使用时,通常优先选 ArrayDeque。
PriorityQueue 是什么?
PriorityQueue 是优先级队列,底层通常是堆。
它不是先进先出,而是每次取出优先级最高的元素。默认按自然顺序,也可以传入 Comparator 自定义优先级。
注意:PriorityQueue 不是线程安全的,也不允许存 null。
BlockingQueue 是什么?
BlockingQueue 是阻塞队列,常用于生产者消费者模型。
当队列为空时,消费者获取元素可以阻塞等待;当队列满时,生产者插入元素可以阻塞等待。
常见方法:
put():队列满时阻塞。take():队列空时阻塞。offer():插入失败返回false,也可以带超时时间。poll():获取不到返回null,也可以带超时时间。
BlockingQueue 常见实现类有哪些?
常见实现:
ArrayBlockingQueue:数组实现的有界阻塞队列。LinkedBlockingQueue:链表实现的阻塞队列,可以有界,也可以近似无界。PriorityBlockingQueue:支持优先级的无界阻塞队列。DelayQueue:延迟队列,元素到期后才能被取出。SynchronousQueue:不存储元素,每个 put 必须等待一个 take。LinkedTransferQueue:链表实现的无界传输队列。
ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
ArrayBlockingQueue 底层是数组,必须指定容量,是有界队列。它通常使用一把锁控制入队和出队。
LinkedBlockingQueue 底层是链表,可以指定容量,不指定时容量很大。它通常使用两把锁分别控制入队和出队,并发能力相对更好。
选择:
- 容量固定、希望内存可控:
ArrayBlockingQueue。 - 吞吐要求更高、生产消费并发较多:
LinkedBlockingQueue。 - 线程池工作队列不要随便用无界
LinkedBlockingQueue,可能堆积任务导致内存压力。
