List 集合
ArrayList
是动态数组, 能够进行扩容, 但线程不安全.
ArrayList 的底层实现是 Object[] 数组, 因此可以存放任何数据类型(包括 null).
构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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);
}
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object [] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
自动扩容
- 每次添加元素(调用
add())去检查添加后是否会超过长度(size == elementData.length), 扩容通过ensureCapacity来实现, 会扩容至原来的 1.5 倍 大小. - 当扩容时, JVM 会直接在当前 Heap 中寻找一块足够容纳扩容后数组大小的内存, 而不是在原位置进行扩容.
- 如果没有满足的连续内存则会触发垃圾回收机制, 如果进行垃圾回收之后依旧没有足够内存则会抛出 OOM (OutOfMemory).
扩容详细过程
grow() (JDK 8)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
[!NOTE] Title 注意:JDK11 移除了
ensureCapacityInternal()和ensureExplicitCapacity()方法, 直接在 add 内modCount++
线程安全性
Collections 中绝大多数的实现非线程安全, ArrayList 也不例外.
| 特性 / 项目 | Vector | ArrayList |
|---|---|---|
| 是否可扩容 | 可以(默认扩容 2 倍) | 可以(默认扩容 1.5 倍) |
| 线程安全性 | 线程安全(所有 public 方法加 synchronized) |
非线程安全 |
| 运行效率 | ❌ 低(同步开销大,即使单线程也加锁) | 高(无同步开销) |
| Fail-Fast 机制 | Fail-Safe(部分版本)或弱 Fail-Fast(早期 JDK 中 Vector 的 Enumeration 不是 fail-fast;Iterator 是 fail-fast) | Fail-Fast(通过 modCount 检查并发修改) |
| 初始化容量 | - 无参构造:默认容量 10- 有参构造:指定容量 | - 无参构造:初始为 0(JDK 8+),首次 add 扩容到 10- 有参构造:指定容量 |
| 扩容策略 | - 未指定增量:扩容为 2 倍- 指定 capacityIncrement > 0:每次增加该值 |
- 默认:newCapacity = old + (old >> 1)(1.5 倍)- 若仍不足,则直接扩到所需最小容量 |
| 是否实现 RandomAccess | 是 | 是 |
LinkedList
LinkedList 底层实现为双向链表, 在 JDK1.7 之前 LinkedList 底层为双向循环链表. 因为底层为链表, 所以每个 node 存储时所占用的内存更多, 需要存储前后节点地址指针.
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;
}
}
LinkedList 插入与删除元素时间复杂度
LinkedList 默认存储着头节点和尾节点的指针.
- 在头尾部插入元素的复杂度为
O(1) - 在其他位置插入元素时选择距离最近的指针遍历到节点处插入, 总时间复杂度为
O(n)
为什么 LinkedList 不支持快速随机访问?
LinkedList 底层为链表, 链表在物理内存中的分布不连续, 只能通过指针进行定位所以不能实现.
| 特性 / 操作 | LinkedList | ArrayList |
|---|---|---|
| 底层结构 | 双向链表(每个节点包含数据 + 前后指针) | 动态数组(Object [] 数组) |
| 内存占用 | 较大(每个元素额外存储两个引用:prev 和 next) | 较小(仅存储元素本身,但可能有未使用的预留空间) |
| 是否实现 RandomAccess | 否 | 是(支持快速随机访问) |
| 头部插入/删除 | O(1)(直接修改头节点指针) | O(n)(需移动所有后续元素) |
| 尾部插入/删除 | O(1)(维护了 tail 指针) | 摊销 O(1)(通常无需移动;扩容时 O(n)) |
| 中间位置插入/删除 | O(n)(需遍历到目标位置) | O(n)(需移动插入点之后的所有元素) |
| 随机访问(get/set) | O(n)(需从头或尾遍历) | O(1)(直接通过索引访问数组) |
| 扩容机制 | 无需扩容(动态链接节点) | 需要扩容(默认增长为原容量的 1.5 倍) |
| 迭代性能 | 略慢(指针跳转,缓存局部性差) | 快(连续内存,缓存友好) |
RandomAccess 接口
一个标记接口,用于告诉算法(如 Collections.binarySearch)该列表是否支持高效随机访问。ArrayList 实现了它,LinkedList 没有。
Vector
TODO: 待补充