【java-core-collections】集合框架深度解析

集合框架深度解析

一、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 已被标记过时。

学习路径

  1. 基础:ArrayList、LinkedList、HashMap 使用
  2. 原理:HashMap 扩容、红黑树转换、hash 计算
  3. 并发:ConcurrentHashMap 实现、CopyOnWriteArrayList
  4. 应用:合理选择集合类型、性能优化
相关推荐
秋雨梧桐叶落莳2 小时前
iOS——抽屉视图详解
开发语言·macos·ui·ios·objective-c·cocoa
郝学胜-神的一滴2 小时前
Qt 高级开发 016:半内存管理机制
开发语言·c++·qt·程序人生·用户界面
一 乐2 小时前
在线考试|基于Springboot的在线考试管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·毕设·在线考试管理系统
Byte Wizard2 小时前
动态内存管理
c语言·开发语言
zzzsde2 小时前
【Linux】线程同步和互斥(5):线程池的实现&&线程安全
linux·运维·服务器·开发语言·算法·安全
无忧.芙桃2 小时前
C语言文件操作
c语言·开发语言
月落归舟2 小时前
Java并发容器与框架
java·开发语言
右耳朵猫AI2 小时前
Golang技术周刊 2026年第20周
开发语言·后端·golang
不吃土豆的马铃薯2 小时前
高性能服务器程序框架详解(包括Reactor,有限状态机等)
linux·服务器·开发语言·网络·c++
Shadow(⊙o⊙)2 小时前
库的制作与原理1.0,库打包,协作,目标文件.o、ELF格式。
linux·运维·服务器·开发语言