手写 ArrayList 和 LinkedList - 深入理解 Java 集合框架
手写 ArrayList 和 LinkedList
一、List 接口定义
1 | package com.sap.collections; |
二、手写 ArrayList
2.1 核心原理
1 | ┌─────────────────────────────────────────────────────────┐ |
2.2 手写代码
1 | package com.sap.collections; |
三、手写 LinkedList
3.1 核心原理
1 | ┌─────────────────────────────────────────────────────────┐ |
3.2 手写代码
1 | package com.sap.collections; |
四、ArrayList vs LinkedList 对比
4.1 时间复杂度对比
| 操作 | ArrayList | LinkedList | 说明 |
|---|---|---|---|
| get(index) | O(1) ⭐ | O(n) | 数组随机访问 vs 链表遍历 |
| add(element) | 均摊 O(1) | O(1) | 数组尾部添加 / 链表尾部添加 |
| add(index, element) | O(n) | O(n) ⭐ | 数组需移动元素,链表查找耗时 |
| remove(index) | O(n) | O(n) ⭐ | 同上 |
| remove(element) | O(n) | O(n) | 都需要遍历查找 |
4.2 面试重点:什么时候用哪个?
1 | 选择 ArrayList 的场景: |
4.3 内存占用对比
1 | ArrayList(100个元素): |
五、易错点记录
- 类型擦除 - 泛型数组
new E[10]是非法的,必须用new Object[10]再强制转换 - 扩容时机 -
size == table.length时才扩容,不是>= - 边界检查 -
add(index)允许index == size(尾部插入),但remove/get/set不允许 - 内存泄漏 -
remove后必须将table[size] = null,否则对象无法被 GC - equals 比较 - 删除元素时用
Objects.equals()而不是==,支持 null 值比较 - 双向链表指针 - 插入/删除时要同时修改
pre和next,容易漏掉 - 空链表判断 - 添加第一个元素时,
tail == null要特殊处理设置head
六、JDK 源码中的优化技巧
6.1 ArrayList 的 ensureCapacity
1 | // JDK 中的扩容逻辑更精细 |
6.2 LinkedList 的双向查找优化
1 | // JDK 源码中同样使用 index < size/2 判断查找方向 |
6.3 快速失败机制(fail-fast)
1 | // JDK 的迭代器有 modCount 检查 |
七、面试高频问题
Q1: ArrayList 扩容为什么是 1.5 倍?
答: 1.5 倍是时间复杂度和空间复杂度的平衡:
- 扩容倍数太小 → 频繁扩容,拷贝开销大
- 扩容倍数太大 → 内存浪费严重
- 1.5 倍可以用位运算
oldCapacity >> 1高效计算
Q2: 为什么 LinkedList 没有索引还能 get(index)?
答: 可以,但效率低。LinkedList 的 get(index) 会遍历链表,时间复杂度 O(n)。它实现 List 接口是为了保持 API 一致性。
Q3: 什么情况下 LinkedList 比 ArrayList 慢?
答:
- 随机访问: LinkedList O(n) vs ArrayList O(1)
- 遍历: LinkedList 缓存不友好,CPU 预读取失效
- 现代 CPU: 数组的连续内存访问性能远超链表
Q4: ArrayList 是线程安全的吗?
答: 不是。多线程环境下需要:
- 使用
Collections.synchronizedList(new ArrayList<>()) - 或使用
CopyOnWriteArrayList(读多写少场景) - 或手动加锁