有序、重复、索引 可以配合小弟数据结构文章结合理解 juejin.cn/post/759271…
| 特性 | ArrayList | LinkedList | Vector |
|---|---|---|---|
| 物理结构 | 连续数组 (Object[]) | 双向链表 (Node) | 连续数组 (Object[]) |
| 扩容倍数 | 1.5 倍 (old + old >> 1) | 无 (按需分配节点) | 2 倍 (或指定增量) |
| 初始化 | 延迟加载 (首次 add 扩容 10) | 立即创建对象 (空链表) | 默认初始化容量为 10 |
| 同步性 | 线程不安全 (异步) | 线程不安全 (异步) | 线程安全 (synchronized) |
| 性能优势 | 随机访问 | 首尾增删 | 兼容老版本代码 |
| 内存占用 | 连续大内存,有预留浪费 | 离散内存,指针占用大 | 连续大内存,有预留浪费 |
1、定义和特点
特点
List 接口是 Collection 的子接口,它在单列集合的基础上增加了“位置”的概念。
(1) 核心特征归纳
- 有序性 (Order):元素存储顺序与插入顺序一致。
- 索引访问 (Index):每个元素都有对应的整数索引(0 到 size-1),支持精确位置操作。
- 可重复性 (Repetition):允许存储多个
equals返回true的元素,也允许存储多个null。 (2) 物理结构决定选型 (重点对比)
在 Java 中,选型本质上是在连续内存数组与离散内存链表之间做权衡。
| 维度 | ArrayList (动态数组) | LinkedList (双向链表) |
|---|---|---|
| 底层数据结构 | 连续的 Object[] 数组 | 离散的 Node 双向节点 |
随机访问 (get) | 极快 ():直接通过地址偏移计算。 | 慢 ():需从头或尾逐个指针跳转。 |
| 首尾操作 | 尾部快 ();头部慢 (,需移动全表)。 | 极快 ():直接修改首尾指针。 |
| 中间插入/删除 | 慢 ():涉及 System.arraycopy 大规模移动。 | 快 ():仅需修改前后节点指针。 |
| 内存开销 | 较低:仅存储元素本身(但有预留空间浪费)。 | 较高:每个数据需额外存储两个指针(prev/next)。 |
| CPU 缓存友好度 | 高:数组连续性利于预取。 | 低:节点散落在堆内存各处。 |
选型结论:
- 若应用场景以 “读多写少” 或 “末尾追加” 为主,首选 ArrayList。
- 若涉及频繁的 “头尾增删” 或 “队列操作”,考虑 LinkedList。
List的api
除了继承自 Collection 的 API 外,List 额外提供了基于索引的操作:
| 操作分类 | 方法签名 | 重点备注 |
|---|---|---|
| 增 | add(int index, E element) | 在指定位置插入,后续元素全部后移。 |
| 删 | remove(int index) | 返回被删除的元素,后续元素前移。 |
| 改 | set(int index, E element) | 替换指定位置元素,返回旧元素。 |
| 查 | get(int index) | 核心方法。List 随机访问的基石。 |
| 搜索 | indexOf(Object o) / lastIndexOf | 查找元素首次/最后一次出现的位置,找不到返回 -1。 |
| 截取 | subList(int from, int to) | 避坑:返回的是原 List 的视图,对 subList 的修改会直接反映在原 List 上。 |
2、ArrayList
ArrayList 是基于动态数组实现的 List 接口实现类。它的核心在于如何通过“预留空间”和“自动扩容”来管理这个非固定长度的数组。
(1) 存储结构与 transient 优化
核心成员变量:
/**
* 存储 ArrayList 元素的数组缓冲区。
* ArrayList 的容量就是该数组缓冲区的长度。
* 任何 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList,
* 在添加第一个元素时都将被扩容至 DEFAULT_CAPACITY(默认容量 10)。
*/
transient Object[] elementData; // 非私有以简化嵌套类访问
/**
* ArrayList 的大小(包含的元素数量)
*/
private int size;
transient 的意义: 由于 elementData 的容量(capacity)通常大于 size,为了避免序列化那些无效的 null 元素造成浪费,ArrayList 标记其为 transient。它手动重写了序列化方法,仅持久化有效数据,从而实现空间利用率的最大化。
(2) 初始化:
延迟分配(Lazy Initialization)策略
在 JDK 1.7 之后,ArrayList 为了优化内存,采用了延迟初始化的方式:
- 空参构造:
List list = new ArrayList();时,底层并不会立即创建长度为 10 的数组。 - 默认指向:它会将
elementData指向一个共享的空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA。 - 首次触发:只有在真正执行第一次
add()操作时,才会将数组扩容至默认容量 10。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
(3) 扩容算法:1.5 倍
当数组空间不足时,会触发 grow(int minCapacity) 方法:
- 计算规则:
int newCapacity = oldCapacity + (oldCapacity >> 1);- 逻辑:旧容量加上旧容量的一半,即 1.5 倍扩容。
- 优化:使用位运算符
>>(右移一位),计算速度极快,且在容量增长与内存开销之间取得了动态平衡。
- 数据迁移:调用
Arrays.copyOf(),其底层最终执行System.arraycopy()(Native 方法),通过 C++ 实现内存块的整体搬迁。
/** * 增加容量以确保它至少能容纳由最小容量参数指定的元素数量。
* @param minCapacity 所需的最小容量
* @throws OutOfMemoryError 如果 minCapacity 小于零
*/
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
// 如果当前容量 > 0,或者不是默认的空数组,则执行 1.5 倍扩容逻辑
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* 最小增长步长 */
oldCapacity >> 1 /* 首选增长步长 (0.5倍旧容量) */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
// 第一次添加元素,直接初始化为 10(或 minCapacity 中的较大者)
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
(4) 核心操作的代码分析
- 查询 (
get):。基于数组下标的寻址公式:。
/**
* 返回此列表中指定位置的元素。
*
* @param index 要返回的元素的索引
* @return 列表中指定位置的元素
* @throws IndexOutOfBoundsException 如果索引越界
*/
public E get(int index) {
// 检查索引是否在有效范围内 (0 <= index < size)
Objects.checkIndex(index, size);
return elementData(index);
}
// 实际访问数组并强转类型
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
- 插入 (
add):- 尾部插入:通常为 ,若触发扩容则为 。
- 中间插入:。需要将插入点后的所有元素整体后移一位。
/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
modCount++; // 结构性修改计数自增
add(e, elementData, size);
return true;
}
/**
* 此辅助方法从 add(E) 中分离出来,以保持方法字节码大小小于 35,
* 这有助于在 C1 编译循环中调用 add(E) 时进行内联优化。
*/
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow(); // 空间已满,触发扩容
elementData[s] = e; // 在末尾赋值
size = s + 1; // 逻辑长度加 1
}
/**
* 在此列表的指定位置插入指定元素。
* 将当前在该位置的元素(如果有)以及所有后续元素向右移动(索引加 1)。
*/
public void add(int index, E element) {
rangeCheckForAdd(index); // 专门为 add 操作设计的边界检查
modCount++;
final int s;
Object[] elementData;
// 1. 检查是否需要扩容
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
// 2. 核心:调用 Native 方法进行内存拷贝,将 index 后的元素后移一位
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
// 3. 覆盖目标位置
elementData[index] = element;
size = s + 1;
}
- 删除 (
remove):。需要将被删元素后的所有元素整体前移,并手动将最后一个索引位设为null以便 GC 回收。
/**
* 内部删除辅助方法,跳过边界检查且不返回被删除的值。
* * @param es 存储元素的数组
* @param i 要删除的索引位
*/
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
// 如果删除的不是最后一个元素,则需要前移覆盖
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
// 关键点:将数组最后一个位置设为 null,让 GC 能够回收不再使用的对象引用
es[size = newSize] = null;
}
3、LinkedList
LinkedList 底层基于 双向链表 实现。由于其物理存储是离散的,它不具备随机访问的优势,但在频繁增删的场景下表现卓越。
(1) 存储结构
LinkedList的每个元素都被封装在一个内部类Node中。LinkedList通过持有头尾两个指针,实现了对整个链表的控制。
private static class Node<E> {
E item; // 实际存储的数据
Node<E> next; // 指向后继节点的指针
Node<E> prev; // 指向前驱节点的指针
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
// 记录当前链表中的节点个数。
transient int size = 0;
// 指向链表的第一个节点。
transient Node<E> first;
// 指向链表的最后一个节点。
transient Node<E> last;
(2) 核心操作的代码分析
1. 查询 (get)
- 复杂度:。
- 原理:由于内存不连续,无法通过公式定位,必须顺着指针一个个找。
- 源码优化:采用了二分折半查找。如果索引在前半段则从
first向后找,在后半段则从last向前找。
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
2. 插入 (add)
- 尾部插入:。直接修改
last指针即可。 - 指定位置插入:。虽然修改指针是 ,但定位到该位置需要 。
/**
* 在指定位置插入元素。
*/
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element); // 直接追到末尾
else
linkBefore(element, node(index)); // 查找到该节点并在其前插入
}
/**
* 将元素 e 链接到非空节点 succ 之前。
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
// 创建新节点,左右缝合
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
3. 删除 (remove)
- 头尾删除:。
- 指定元素删除:。需要先查找到该元素。
/**
* 取消链接非空节点 x(内部删除逻辑)。
*/
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) { // 如果是头节点
first = next;
} else {
prev.next = next;
x.prev = null; // 辅助 GC
}
if (next == null) { // 如果是尾节点
last = prev;
} else {
next.prev = prev;
x.next = null; // 辅助 GC
}
x.item = null; // 辅助 GC
size--;
modCount++;
return element;
}
4、Vector (不推荐)
Vector 是 JDK 1.0 就存在的“元老级”集合。它的底层存储逻辑与 ArrayList 几乎一致(都是基于 Object[] 数组),但其核心设计目标是线程安全。
(1) 存储与扩容机制
- 物理结构:同样是
transient Object[] elementData。 - 扩容差异:
ArrayList固定扩容为旧容量的 1.5 倍。Vector可以通过构造函数指定capacityIncrement(增长增量)。如果未指定,默认扩容为旧容量的 2 倍。
(2) 核心特性:线程安全
Vector 几乎所有对外暴露的操作方法(add, get, remove, size 等)都加上了 synchronized 关键字。
/**
* 将指定元素追加到此向量的末尾。
*/
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
/**
* 返回此向量中指定位置的元素。
*/
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
- 代价:由于采用了全表锁(即任何时候只有一个线程能操作该集合),在高并发场景下性能极差。
(3) 重点总结:Vector vs ArrayList
| 特性 | ArrayList | Vector |
|---|---|---|
| 诞生版本 | JDK 1.2 (新) | JDK 1.0 (旧) |
| 线程安全 | 线程不安全,性能高 | 线程安全,但性能低 |
| 扩容倍数 | 1.5 倍 | 2 倍 |
| 推荐程度 | 首选 | 不推荐 (除非有特定陈旧代码维护需求) |