集合框架深度解析
一、List 集合
1.1 ArrayList
底层数据结构: Object[] 数组
java
// 扩容机制
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量 = 旧容量 * 1.5
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, oldCapacity >> 1);
return Arrays.copyOf(elementData, newCapacity);
}
扩容流程:
初始: 空数组 {} (懒初始化)
首次 add: 默认容量 10
第 11 次 add: 10 → 15 (×1.5)
第 16 次 add: 15 → 22 (×1.5)
时间复杂度:
| 操作 | 复杂度 | 原因 |
|---|---|---|
| get(index) | O(1) | 数组随机访问 |
| add(e) 尾部 | O(1) 均摊 | 偶尔扩容 O(n) |
| add(index, e) | O(n) | 需要移动元素 |
| remove(index) | O(n) | 需要移动元素 |
1.2 ArrayList vs LinkedList
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 头部插入 | O(n) | O(1) |
| 内存占用 | 紧凑 | 每节点额外2个指针 |
| 缓存友好性 | 好(连续内存) | 差(分散内存) |
结论: 99% 场景用 ArrayList,除非频繁头部插入。
1.3 ArrayList 线程安全问题
java
// ConcurrentModificationException
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if ("b".equals(s)) list.remove(s); // 抛异常!
}
// 正确方式1: Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("b".equals(it.next())) it.remove();
}
// 正确方式2: CopyOnWriteArrayList
List<String> safeList = new CopyOnWriteArrayList<>(list);
二、Map 集合
2.1 HashMap 原理
数据结构:数组 + 链表 + 红黑树(JDK 8+)
table[]
┌────┐
│ 0 │ → null
├────┤
│ 1 │ → Node(K1,V1) → Node(K2,V2) → Node(K3,V3)
├────┤
│ 2 │ → TreeNode (红黑树)
├────┤
│ 3 │ → null
├────┤
│... │
└────┘
put 流程:
1. 计算 hash = (h = key.hashCode()) ^ (h >>> 16) // 扰动函数
2. 计算索引 = (n - 1) & hash // 等价 hash % n
3. 桶为空 → 直接放入
4. 桶非空:
- key 相同 → 覆盖 value
- 是 TreeNode → 红黑树插入
- 是链表 → 尾插法,长度 ≥ 8 时转红黑树
5. 检查容量 > threshold → 扩容
2.2 HashMap 扩容机制
java
// 核心参数
static final int DEFAULT_INITIAL_CAPACITY = 16; // 默认容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 负载因子
static final int TREEIFY_THRESHOLD = 8; // 链表→红黑树
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树→链表
static final int MIN_TREEIFY_CAPACITY = 64; // 树化最小容量
// 扩容条件
size > capacity * loadFactor → capacity × 2
为什么容量是 2 的幂?
(n - 1) & hash等价于hash % n,位运算更快- 扩容时,元素要么在原位置,要么在原位置+旧容量,无需重新计算 hash
2.3 JDK 7 vs JDK 8 HashMap
| 特性 | JDK 7 | JDK 8 |
|---|---|---|
| 链表插入 | 头插法(多线程成环) | 尾插法 |
| 链表过长 | 始终链表 | 转 TreeNode(红黑树) |
| hash 计算 | 9 次扰动 | 1 次扰动 |
| 扩容 | 重新计算所有位置 | 原位置 or 原位置+旧容量 |
2.4 HashMap 为何线程不安全
JDK 7: 并发扩容时头插法导致链表成环 → 死循环
JDK 8: 并发 put 时数据覆盖
java
// 两个线程同时判断桶为空,都执行写入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 后一个覆盖前一个
2.5 ConcurrentHashMap(JDK 8)
java
// 初始化(CAS)
if (tab == null || tab.length == 0)
tab = initTable(); // CAS 保证只有一个线程初始化
// put 操作
Node<K,V> f; int n, i;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶为空:CAS 写入
if (casTabAt(tab, i, null, new Node<>(hash, key, value)))
break;
} else {
// 桶非空:synchronized 头节点
synchronized (f) {
// 遍历链表/红黑树插入
}
}
三、Set 集合
3.1 HashSet
java
// HashSet 底层就是 HashMap
public class HashSet<E> {
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}
3.2 TreeSet
- 底层 TreeMap(红黑树)
- 元素有序(自然排序或 Comparator)
- 添加/删除/查询 O(log n)
四、Queue 集合
4.1 PriorityQueue
java
// 底层是最小堆(数组实现)
// offer: 上浮(sift up) O(log n)
// poll: 下沉(sift down) O(log n)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(3); pq.offer(1); pq.offer(2);
pq.poll(); // 1(最小元素)
4.2 阻塞队列
| 队列 | 特点 | 适用场景 |
|---|---|---|
| ArrayBlockingQueue | 有界数组 | 线程池生产者消费者 |
| LinkedBlockingQueue | 可选有界链表 | Executors 默认 |
| SynchronousQueue | 不存储元素 | 直接传递任务 |
| PriorityBlockingQueue | 无界优先级 | 优先级任务 |
| DelayQueue | 延迟取出 | 定时任务 |
java
// 生产者-消费者模式
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// 生产者
queue.put("任务"); // 满时阻塞
// 消费者
String task = queue.take(); // 空时阻塞
五、面试高频题
Q1: HashMap 的底层数据结构?
考点 :HashMap 原理
难度 :⭐⭐⭐
答案:JDK 8 是数组 + 链表 + 红黑树。数组默认容量 16,负载因子 0.75。hash 冲突时形成链表,链表长度 ≥ 8 且数组长度 ≥ 64 转为红黑树,红黑树节点 ≤ 6 退回链表。
Q2: HashMap 的 hash 方法为什么用扰动函数?
考点 :hash 优化
难度 :⭐⭐⭐
答案 :(h ^ (h >>> 16)) 让高 16 位也参与索引计算,减少碰撞。因为容量通常较小,只有低位参与索引,高位不同但低位相同的 key 会冲突。
Q3: HashMap 和 ConcurrentHashMap 的区别?
考点 :并发容器
难度 :⭐⭐⭐
答案:HashMap 线程不安全;ConcurrentHashMap 线程安全。JDK 8 CHM 使用 CAS + synchronized(锁头节点),粒度细到桶级别,并发度等于数组长度。
Q4: ArrayList 的扩容机制?
考点 :动态数组
难度 :⭐⭐
答案:默认初始容量 10(首次 add 时),每次扩容为原来的 1.5 倍。使用 Arrays.copyOf 复制旧数组到新数组。
Q5: HashMap 为什么用红黑树而不是 AVL 树?
考点 :数据结构选择
难度 :⭐⭐⭐⭐
答案:红黑树插入/删除时旋转次数更少(最多 3 次 vs AVL 的 O(log n) 次),综合增删查性能更好。HashMap 场景增删频繁,红黑树更合适。
Q6: fail-fast 和 fail-safe 的区别?
考点 :迭代器机制
难度 :⭐⭐⭐
答案:fail-fast(ArrayList/HashMap):遍历时修改抛 ConcurrentModificationException,通过 modCount 检测。fail-safe(CopyOnWriteArrayList/ConcurrentHashMap):遍历副本或弱一致性,允许并发修改。
Q7: Comparable 和 Comparator 的区别?
考点 :排序接口
难度 :⭐⭐
答案:Comparable 在类内部实现 compareTo(),定义自然排序。Comparator 是外部比较器,实现 compare(),可以定义多种排序策略。Comparator 更灵活,符合开闭原则。
Q8: CopyOnWriteArrayList 的原理和适用场景?
考点 :并发容器
难度 :⭐⭐⭐
答案:写入时复制整个数组。读无锁,写加锁并复制。适合读多写极少(事件监听器列表、配置列表)。不适用写频繁场景(每次 O(n) 复制)。迭代器遍历的是快照。
Q9: HashMap 的负载因子为什么是 0.75?
考点 :HashMap 参数
难度 :⭐⭐⭐⭐
答案:时间和空间的折中。0.5 太浪费空间;1.0 碰撞太多。0.75 时泊松分布计算,每个桶 8 个以上元素的概率 < 千万分之一。也是 0.75 浮点数在二进制中精确表示。
Q10: ConcurrentHashMap 能完全替代 Hashtable 吗?
考点 :并发容器
难度 :⭐⭐⭐
答案:基本可以。ConcurrentHashMap 性能远优于 Hashtable(后者每个方法都 synchronized)。但 Hashtable 支持 null key/value,CHM 不支持。Hashtable 已被标记过时。
学习路径
- 基础:ArrayList、LinkedList、HashMap 使用
- 原理:HashMap 扩容、红黑树转换、hash 计算
- 并发:ConcurrentHashMap 实现、CopyOnWriteArrayList
- 应用:合理选择集合类型、性能优化