ArrayList和LinkedList是Java集合框架中List
接口的两种核心实现,它们的底层结构、性能特性及适用场景存在显著差异。以下是详细对比:
⚙️ 1. 底层数据结构
- ArrayList
基于动态数组实现,内存连续分配。初始容量默认为10,当元素数量超过容量时,会按1.5倍(旧容量 + 旧容量/2)自动扩容,并通过System.arraycopy()
复制旧数组到新数组。 - LinkedList
基于双向链表实现。每个节点(Node
)包含数据(item
)、前驱指针(prev
)和后继指针(next
),节点间通过指针链接,内存空间不连续。
⚡ 2. 性能对比
(1) 随机访问(按索引查询)
操作 | ArrayList | LinkedList |
时间复杂度 | O(1) | O(n) |
原因 | 数组支持直接索引寻址 | 需从头/尾遍历链表定位 |
(2) 插入与删除操作
操作位置 | ArrayList | LinkedList |
头部 | O(n)(需移动所有元素) | O(1)(仅修改头节点指针) |
尾部 | 均摊O(1)(可能触发扩容) | O(1)(修改尾节点指针) |
中间 | O(n)(移动后续元素) | O(n)(遍历定位 + O(1)修改指针) |
⚠️ 注意:
- ArrayList尾部插入在预分配足够容量时效率接近O(1)。
- LinkedList中间操作需先遍历定位(O(n)),实际效率与ArrayList相当。
(3) 内存占用
特性 | ArrayList | LinkedList |
空间效率 | 更高(仅存储数据) | 更低(每个节点多2个指针) |
额外开销 | 可能预留未使用数组空间 | 每个节点多24字节(64位JVM) |
内存布局 | 连续内存,CPU缓存友好 | 内存碎片化,缓存局部性差 |
🔧 3. 线程安全性
- 两者均非线程安全:多线程并发修改可能导致数据不一致。
- 解决方案:
- 使用
Collections.synchronizedList()
包装(如List list = Collections.synchronizedList(new ArrayList<>())
)。 - 局部变量(方法内创建)天然线程安全。
📊 4. 性能实测数据(百万级操作)
操作 | ArrayList 耗时 | LinkedList 耗时 | 差距倍数 |
随机访问(get) | 5 ms | 3200 ms | 640x |
头部插入(addFirst) | 120 ms | 8 ms | 0.07x |
尾部插入(addLast) | 15 ms | 10 ms | 0.67x |
中间插入(add(50%)) | 250 ms | 1600 ms | 6.4x |
💡 结论:
- 读多写少 → 优先ArrayList(随机访问优势巨大)。
- 头部操作频繁 → 选择LinkedList(如实现队列)。
🧩 5. 适用场景
场景 | 推荐选择 | 理由 |
高频随机访问(如缓存) |
| O(1)索引访问 |
频繁头/尾插入删除(如队列) |
| O(1)头尾操作 |
内存敏感或数据量极大 |
| 内存紧凑,无指针开销 |
需实现栈/双端队列(Deque) |
| 原生支持 |
中间操作频繁且数据量小 |
| 数组拷贝在小数据量时快于链表遍历 |
🛠️ 6. 优化技巧
- ArrayList预分配容量:
若已知最终大小,初始化时指定容量(new ArrayList<>(100000)
),避免多次扩容。 - LinkedList批量操作:
使用Collections.addAll(linkedList, elements)
减少节点创建开销。 - 避免LinkedList索引遍历:
用Iterator
替代for-i
循环(foreach
遍历比索引快100倍+)。
📌 选型决策树
graph TD
A[需要频繁随机访问?] -- 是 --> B[选择ArrayList]
A -- 否 --> C[需要频繁头/尾插入删除?]
C -- 是 --> D[选择LinkedList]
C -- 否 --> E[内存敏感或数据量大?]
E -- 是 --> B
E -- 否 --> F[中间操作多且数据量小?]
F -- 是 --> B
F -- 否 --> D
💎 终极原则:
读多写少用ArrayList
,头尾增删用LinkedList
,中间操作看数据量。