[ArrayList 与 LinkedList 的核心区别](#ArrayList 与 LinkedList 的核心区别)
[ArrayList 源码深度解析](#ArrayList 源码深度解析)
[核心方法:add(E e) 与扩容机制](#核心方法:add(E e) 与扩容机制)
[第三步:核心扩容逻辑 grow()](#第三步:核心扩容逻辑 grow())
前言:
在Java开发中,List接口是最常用的集合接口之一,而ArrayList和LinkedList则是它的两个核心实现类。很多初学者甚至工作多年的开发者,往往只知道"ArrayList查询快,增删慢 ;LinkedList增删快,查询慢"这句口诀。
本文将摒弃空谈 ,从底层数据结构 、源码实现 以及性能分析 三个维度,彻底搞清楚这两者的区别,并重点深入剖析ArrayList的源码,看看它究竟是如何工作的。
ArrayList 与 LinkedList 的核心区别
在开始看源码之前,我们先通过一张表快速建立直观的认知:
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 底层数据结构 | 动态数组 | 双向链表 |
| 内存占用 | 连续内存空间,可能浪费尾部空间 | 非连续内存,每个节点需存储前后指针,内存占用更大 |
| 随机访问 | O(1),效率极高 | O(n),需遍历链表,效率低 |
| 插入/删除 | O(n),涉及数组元素的复制移动 | O(n) ,虽然找到位置需要遍历,但操作本身只需修改指针指向;若在头尾操作,效率极高为O(1) |
| 尾部插入 | O(1) (均摊),需考虑扩容 | O(1) |
| 迭代效率 | 较高,对CPU缓存友好 | 较低,节点在内存中分散,缓存命中率低 |
比较他们不同的时候从,底层结构,性能,内存,线程安全来说。他们线程都不安全。
一句话总结:
绝大多数业务场景(以遍历和随机访问为主),ArrayList 完胜 LinkedList。只有在频繁头部插入/删除且数据量极大时,LinkedList 才有理论上的优势。
ArrayList 源码深度解析
环境基于 JDK 1.8
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组实例,用于构造方法指定初始容量为0时
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量的空数组实例,用于无参构造,用于区分第一次添加元素时的扩容逻辑
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 真正存储元素的数组,使用 transient 修饰,不参与默认序列化
transient Object[] elementData;
// 集合中实际元素个数
private int size;
}
构造方法:初始化时到底有没有创建10个大小的数组?
这是一个经典的面试题。
情况一:无参构造
java
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
此时,elementData 指向的是一个空数组,并没有初始化容量为10。延迟加载(懒加载)策略,等到第一次 add 时才真正分配空间。
情况二:指定容量构造
java
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
可以看见,如果指定了大于0的数,那么就会直接创造指定大小的数组,如果指定容量为 10,则会立即创建一个长度为 10 的数组。
核心方法:add(E e) 与扩容机制
这是 ArrayList 最精髓的部分。
java
public boolean add(E e) {
// 1. 确保内部容量足够(核心扩容逻辑)
ensureCapacityInternal(size + 1); // 传入最小需要容量 minCapacity = size + 1
// 2. 赋值
elementData[size++] = e;
return true;
}
第一步:计算容量
java
private void ensureCapacityInternal(int minCapacity) {
// 如果当前数组是默认的空数组(无参构造创建的)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 取 DEFAULT_CAPACITY (10) 和 minCapacity 的最大值
// 第一次 add 时,minCapacity = 1,所以这里会返回 10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 进入真正的扩容判断
ensureExplicitCapacity(minCapacity);
}
第二步:判断是否需要扩容
java
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 记录结构修改次数,用于快速失败机制(fail-fast)
// 如果需要的容量 大于 当前数组的长度,则必须扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
第三步:核心扩容逻辑 grow()
java
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 关键点:新容量 = 旧容量 + 旧容量 >> 1 (即原来的 1.5 倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容 1.5 倍后还是小于最小需求容量(这种情况极少,发生在初始容量为0或1时)
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量超过了规定的最大数组容量 (Integer.MAX_VALUE - 8)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 核心:数组复制,产生一个新的数组,将原数据拷贝过去
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容机制总结:
触发时机 :当**
size + 1** 大于当前数组长度时。扩容大小 :变为原来的 1.5 倍 (
oldCapacity + (oldCapacity >> 1))。性能影响 :扩容涉及
Arrays.copyOf,这是一个耗时的 O(n) 操作。如果能预估数据量,使用new ArrayList<>(int capacity)指定大小,可以避免频繁扩容,显著提升性能。
如何选择
优先使用 ArrayList :在绝大多数业务开发中(展示列表、查询数据、批量插入后仅遍历),
ArrayList是唯一正确的选择。
使用 LinkedList 的场景:
实现 队列(Queue) 或 双端队列(Deque) 结构,利用
offer、poll、peek等 API。需要频繁在列表头部进行插入或删除,且数据量极大(百万级以上)。
避免的错误:
不要使用
LinkedList存储大量数据仅仅因为你需要偶尔的中间删除。使用
ArrayList时,如果能预估数据量,务必指定初始容量,避免频繁扩容带来的性能损耗。
总结
底层结构 :
ArrayList是数组,LinkedList是双向链表。性能 :
ArrayList查询快,中间增删慢;LinkedList查询慢,头尾增删快。源码重点 :
ArrayList的扩容机制 是 1.5 倍,无参构造延迟初始化。场景 :开发中 90% 以上的场景用 ArrayList ,只有在需要实现队列或者极端高频的头尾操作时,才考虑
LinkedList。
