每日一句:要去做那些正确的事,而不是容易的事儿
系列介绍
今天是Java集合的第一篇。
"Java集合"系列!本系列旨在帮助Java开发者系统性地准备面试,每天精选至少10道经典面试题,涵盖Java集合、进阶、框架等各方面知识。坚持学习21天,助你面试通关!
基础面试题:
Java集合
1. Collection 和 Map 接口的区别是什么?
核心区别:数据结构与元素组织方式
- Collection:存储单元素集合,包含三大子接口:
List
:有序可重复(索引访问)Set
:无序唯一(基于hashCode/equals
)Queue
:队列结构(FIFO/优先级)
- Map:存储键值对(Key-Value)映射,Key 唯一(基于
hashCode/equals
),Value 可重复。 - 关键设计差异:
- Collection 使用
add()
/remove()
操作元素;Map 使用put()
/get()
操作键值对。 - Map 不是 Collection 的子接口,提供独立的
keySet()
、values()
和entrySet()
视图。
- Collection 使用
- 哲学差异:
Collection 是线性/集合抽象,Map 是关联数组抽象(如字典)。两者解决不同领域问题,故设计为独立接口。
2. List、Set、Queue 的区别是什么?
特性 | List | Set | Queue |
---|---|---|---|
顺序 | 插入有序(支持索引) | 无序(LinkedHashSet/TreeSet除外) | 按规则排序(FIFO/优先级) |
唯一性 | 允许重复元素 | 元素唯一(hashCode/equals ) | 可重复(LinkedList除外) |
Null 支持 | 允许多个 null | 最多一个 null(TreeSet 不允许) | 部分实现允许(如LinkedList) |
典型实现 | ArrayList, LinkedList | HashSet, TreeSet | LinkedList, PriorityQueue |
用途 | 序列化数据存储 | 去重集合 | 任务调度/消息传递 |
3. Set 接口是如何保证元素唯一性的?
底层机制:hashCode()
与 equals()
协同
- 添加元素流程:
- 调用元素的
hashCode()
计算哈希值 → 定位桶位置(Bucket)。 - 若桶为空:直接存入。
- 若桶非空:遍历桶内元素,调用
equals()
逐一同新元素比较。- 存在相等元素(
equals()
返回true
):拒绝添加。 - 无相等元素:存入桶中(链表或红黑树)。
- 存在相等元素(
- 调用元素的
- 实现依赖:
HashSet
基于HashMap
(元素作 Key,固定 Object 作 Value)。TreeSet
基于TreeMap
(通过Comparator
或Comparable
排序去重)。
- 开发者契约:
必须正确重写hashCode()
和equals()
(相同对象必须有相同哈希值,否则破坏 Set 唯一性)。
4. 为什么 Collection 不直接继承 Cloneable 和 Serializable?
设计哲学:接口最小化原则
- 职责分离:
Collection 接口专注定义集合操作(增删查改),克隆与序列化是实现细节,不应强制所有集合支持。 - 灵活性:
- 不是所有集合需要克隆(如只读集合)。
- 序列化可能涉及安全限制(如敏感数据集合)。
- 实践方案:
具体实现类按需实现接口:ArrayList
实现Serializable
和Cloneable
。- 自定义集合可选择不实现。
- 结论:
避免“接口污染”,保持 Collection 简洁性,符合高内聚低耦合原则。
5. Iterator 和 List Iterator 的区别?
能力 | Iterator | ListIterator |
---|---|---|
遍历方向 | 单向(仅 next() ) | 双向(next() / previous() ) |
遍历范围 | 所有 Collection 实现 | 仅 List 及其实现类 |
元素修改 | 支持 remove() | 支持 add() / set() / remove() |
索引访问 | 不支持 | 支持 nextIndex() / previousIndex() |
并发安全 | 均非线程安全 |
代码示例:
ListIterator<String> listIter = list.listIterator();
while (listIter.hasNext()) {
String item = listIter.next();
listIter.set(item + "_modified"); // 修改当前元素
listIter.add("inserted"); // 插入新元素
}
6. fail-fast 和 fail-safe 机制的区别?
特性 | fail-fast | fail-safe |
---|---|---|
工作原理 | 直接操作原集合,迭代时检查修改 | 操作集合副本/快照 |
修改检测 | 通过 modCount 校验(expectedModCount != modCount ) | 无修改检查 |
异常行为 | 抛出 ConcurrentModificationException | 不抛异常,继续迭代 |
数据一致性 | 强一致(实时反映修改) | 弱一致(迭代开始后修改不可见) |
实现类 | ArrayList, HashMap, Vector | CopyOnWriteArrayList, ConcurrentHashMap |
适用场景 | 单线程环境 | 高并发读多写少场景 |
7. ArrayList 的扩容机制是怎样的?默认初始容量是多少?
默认初始容量:10
扩容流程(以添加元素为例):
- 检查当前容量:
if (size == elementData.length)
- 计算新容量:
int newCapacity = oldCapacity + (oldCapacity >> 1)
(即 1.5 倍)。 - 处理边界:
- 最小容量需求:若
newCapacity < minCapacity
(如一次添加多个元素),则newCapacity = minCapacity
。 - 最大容量限制:若
newCapacity > MAX_ARRAY_SIZE
(Integer.MAX_VALUE - 8
),调用hugeCapacity()
处理(可能扩容至Integer.MAX_VALUE
)。
- 最小容量需求:若
- 数据迁移:
Arrays.copyOf(elementData, newCapacity)
(耗时操作)。
示例:
初始容量 10 → 满后扩容至 15 → 再满后扩容至 22(15*1.5)。
8. LinkedList 为什么可以用作队列(Queue)?
结构特性与接口实现:
- 双向链表结构:
Node<E>
节点包含前驱/后继指针,支持 O(1) 时间复杂度的头尾操作。 - 实现 Queue 接口:
直接提供队列操作:- 队尾插入:
add(E e)
/offer(E e)
- 队头移除:
remove()
/poll()
- 查看队头:
element()
/peek()
- 队尾插入:
- 对比数组队列:
无需扩容复制,无容量限制(基于链表动态增长),但内存开销更大(每个元素含指针)。
9. Vector 为什么是线程安全的?它的锁粒度是怎样的?
线程安全机制:方法级 synchronized 锁
- 锁粒度:在方法声明添加
synchronized
关键字(如public synchronized boolean add(E e)
),相当于锁定 整个 Vector 实例(对象锁)。 - 问题:
粗粒度锁导致高并发场景下性能低下(即使独立操作如get(0)
和get(1)
也需串行执行)。 - 替代方案:
Collections.synchronizedList()
:提供同步包装器(锁粒度同 Vector)。CopyOnWriteArrayList
:写时复制(读无锁,写加锁)。ConcurrentLinkedQueue
:CAS 无锁队列。
10. ArrayList 和 CopyOnWriteArrayList 的区别?
维度 | ArrayList | CopyOnWriteArrayList |
---|---|---|
线程安全 | 非线程安全 | 线程安全 |
锁机制 | 无锁(快速失败) | 写操作加 ReentrantLock |
数据存储 | 动态数组 | 写时复制(每次修改创建新数组) |
迭代器行为 | fail-fast(检测修改抛异常) | fail-safe(基于初始数组快照) |
读性能 | O(1) 随机访问(极快) | O(1) 随机访问(无锁) |
写性能 | O(1) 均摊(尾部插入) | O(n) 复制开销(写慢) |
适用场景 | 单线程 / 读多写少(需同步控制) | 高并发读,极少写(如监听器列表) |
11. 为什么 LinkedList 的随机访问性能差?时间复杂度是多少?
根本原因:链表物理非连续存储
- 访问流程:
需从链表头(或尾)遍历至目标索引位置:Node<E> node(int index) { if (index < (size >> 1)) { // 前半部分:从头遍历 Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; } else { // 后半部分:从尾遍历 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; } return x; }
- 时间复杂度:
- 平均 O(n):最坏情况需遍历整个链表(如访问末尾元素)。
- 对比 ArrayList:O(1) 通过索引直接寻址。
- 优化建议:
频繁随机访问用 ArrayList;频繁插入删除用 LinkedList。