Java集合(1)
作者:没有四次元口袋的蓝胖
日期:2026-06-30
标签:Java, 集合框架, ArrayList
一、三者总体对比
1.1 一句话总结
| 集合 | 底层结构 | 特点 | 线程安全 |
|---|---|---|---|
| ArrayList | Object 数组 | 查询快、增删慢 | ❌ |
| LinkedList | 双向链表 | 查询慢、增删快 | ❌ |
| Vector | Object 数组 | 老古董,效率低 | ✅(全表加锁) |
面试题高频: ArrayList 和 LinkedList 的区别是集合篇的"送分题",必须张口就来。
二、ArrayList
2.1 底层结构
ArrayList 底层是 Object 数组,支持动态扩容。
java
// JDK 源码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 存储元素的数组
transient Object[] elementData;
// 元素个数(不是数组长度!)
private int size;
}
注意两个概念的区别:
elementData.length:数组容量(能装多少)size:实际元素个数(已经装了多少)
2.2 构造方法
java
// 1. 无参构造(默认空数组,第一次添加时才初始化容量为10)
ArrayList<String> list1 = new ArrayList<>();
// 2. 指定初始容量
ArrayList<String> list2 = new ArrayList<>(20);
// 3. 传入另一个集合
ArrayList<String> list3 = new ArrayList<>(anotherCollection);
JDK 7 vs JDK 8 的区别:
- JDK 7:无参构造直接创建容量为 10 的数组(饿汉式)
- JDK 8:无参构造创建空数组 {},第一次 add 时才扩容到 10(懒汉式,节省内存)
2.3 常用方法
java
ArrayList<String> list = new ArrayList<>();
// 增
list.add("A"); // 末尾添加
list.add(0, "B"); // 指定位置插入
list.addAll(anotherList); // 批量添加
// 删
list.remove("A"); // 按元素删除(删第一个匹配的)
list.remove(0); // 按下标删除
list.clear(); // 清空
// 改
list.set(0, "C"); // 修改指定位置
// 查
list.get(0); // 根据下标获取(O(1),很快)
list.size(); // 元素个数
list.contains("A"); // 是否包含
list.indexOf("A"); // 第一次出现的下标
list.isEmpty(); // 是否为空
// 遍历
for (int i = 0; i < list.size(); i++) {
String s = list.get(i); // 支持 RandomAccess,普通for最快
}
for (String s : list) { // 增强for(底层迭代器)
}
list.forEach(System.out::println); // Java 8+ forEach
2.4 ArrayList 的优缺点
优点:
- 查询快(下标直接定位,O(1))
- 支持随机访问(实现了 RandomAccess 接口)
- 内存连续,缓存命中率高
缺点:
- 增删慢(需要移动元素,O(n))
- 容量不够时需要扩容,有开销
- 中间插入/删除效率低,尾部操作还可以
三、ArrayList 扩容机制
3.1 扩容流程总览
add(element)
↓
确保容量够不够?
├── 够 → 直接放,size++
└── 不够 → 扩容
├── 新容量 = 旧容量 × 1.5(右移一位)
├── 创建新数组
├── 把旧数组元素复制到新数组(Arrays.copyOf)
└── 再把新元素放进去
3.2 分析
第一步:add 方法入口
java
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量够
elementData[size++] = e; // 放入元素,size+1
return true;
}
第二步:计算最小需要的容量
java
private void ensureCapacityInternal(int minCapacity) {
// 如果当前是空数组(无参构造刚创建),最小容量取默认10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
// DEFAULT_CAPACITY = 10
}
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; // 旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量 = 旧的 1.5 倍
// 右移一位 = 除以2取整,所以 10→15,15→22,22→33...
if (newCapacity - minCapacity < 0) // 扩容后还不够?
newCapacity = minCapacity; // 直接用需要的容量
if (newCapacity - MAX_ARRAY_SIZE > 0) // 超过最大限制?
newCapacity = hugeCapacity(minCapacity); // 特殊处理
elementData = Arrays.copyOf(elementData, newCapacity); // 复制到新数组
}
3.3 扩容的关键点
| 问题 | 答案 |
|---|---|
| 默认初始容量 | 10(JDK 8 懒加载,第一次 add 才初始化为10) |
| 扩容倍数 | 1.5 倍(oldCapacity + (oldCapacity >> 1)) |
| 扩容方式 | Arrays.copyOf() → 底层 System.arraycopy() native 方法 |
| 扩容时机 | 添加元素时,容量不够就扩 |
| 最大容量 | Integer.MAX_VALUE(或 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8) |
3.4 为什么是 1.5 倍?
- 1.5 倍的增长比较温和:不会一次扩太多浪费空间,也不会扩得太频繁
- 2倍的问题:增长太快,空间浪费严重。而且每次都是2倍,之前释放的空间都放不下新数组(内存碎片问题)
- 1.5倍可以复用之前的内存空间:扩到第4次时,前面释放的空间总和已经能装下新数组了
3.5 优化建议:指定初始容量
java
// ❌ 不好:如果要放1000个元素,会触发多次扩容
ArrayList<Integer> list = new ArrayList<>();
// ✅ 好:预估一下大小,直接指定初始容量
ArrayList<Integer> list = new ArrayList<>(1000);
为什么重要? 扩容需要复制整个数组,元素多了开销很大。能预估大小就指定,避免频繁扩容。
📌 常见面试题
Q:ArrayList 怎么扩容的?
默认初始容量10(JDK8懒加载),不够时扩为原来的1.5倍,用 Arrays.copyOf 复制到新数组。
Q:ArrayList 初始容量是多少?
JDK 7 无参构造直接创建长度为10的数组;JDK 8 无参构造创建空数组,第一次 add 时才扩容到10。
Q:ArrayList 和数组的区别?
- 数组大小固定,ArrayList 可以动态扩容
- 数组可以存基本类型和引用类型,ArrayList 只能存引用类型(基本类型要装箱)
- ArrayList 提供了更多方法(add/remove/contains 等)
- 数组效率更高(没有自动装箱和扩容开销)
四、LinkedList 详解
4.1 底层结构
LinkedList 底层是双向链表,每个节点保存数据、前驱和后继指针。
java
// JDK 源码
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
// 头节点
transient Node<E> first;
// 尾节点
transient Node<E> last;
// 元素个数
transient int size;
// 节点内部类
private static class Node<E> {
E item; // 数据
Node<E> next; // 后继
Node<E> prev; // 前驱
}
}
LinkedList 同时实现了 List 和 Deque 接口,所以它既是 List 也是队列(双端队列)。
4.2 常用方法
java
LinkedList<String> list = new LinkedList<>();
// ---- List 相关 ----
list.add("A"); // 尾部添加
list.addFirst("B"); // 头部添加
list.addLast("C"); // 尾部添加
list.add(2, "D"); // 指定位置插入
list.get(0); // 获取(需要遍历,慢)
list.remove(0); // 删除
list.set(0, "E"); // 修改
// ---- Deque 双端队列相关 ----
list.offer("X"); // 尾部入队
list.offerFirst("Y"); // 头部入队
list.offerLast("Z"); // 尾部入队
list.poll(); // 头部出队
list.peek(); // 查看头部
// ---- Stack 栈相关 ----
list.push("top"); // 压栈
list.pop(); // 弹栈
list.peek(); // 查看栈顶
4.3 增删真的快吗?
头插和尾插: O(1),因为有 first 和 last 指针,直接操作,很快 ✅
指定位置插入: 不一定快!
java
// add(int index, E element)
// 1. 先遍历找到 index 位置的节点 → O(n)
// 2. 再修改指针插入 → O(1)
// 整体还是 O(n)!
真相: LinkedList 只有头尾增删是 O(1),中间位置增删要先找位置,时间复杂度还是 O(n),只不过比 ArrayList 少了数组复制的开销。
4.4 get(int index) 的优化 ------ 折半查找
java
Node<E> node(int 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;
}
}
LinkedList 的查询做了优化:从近的那一头开始找,最坏情况找 n/2 次,但本质还是 O(n)。
4.5 LinkedList 的优缺点
优点:
- 头尾增删快(O(1))
- 不需要扩容,用多少 new 多少
- 可以当队列/栈用(实现了 Deque 接口)
缺点:
- 查询慢(不支持随机访问,O(n))
- 每个节点要存前驱和后继指针,内存占用大
- 内存不连续,缓存命中率低
五、Vector 详解
5.1 基本介绍
Vector 是 JDK 1.0 就有的老类,ArrayList 是 JDK 1.2 才出来的。
Vector 所有方法都加了 synchronized,所以线程安全但效率低,基本被淘汰了。
java
public synchronized boolean add(E e) { ... }
public synchronized E get(int index) { ... }
public synchronized E remove(int index) { ... }
5.2 扩容机制
java
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// capacityIncrement 默认是 0 → 扩容为 2 倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// ...
}
| 特性 | ArrayList | Vector |
|---|---|---|
| 线程安全 | ❌ | ✅(synchronized 全方法加锁) |
| 扩容倍数 | 1.5 倍 | 2 倍(可设置) |
| 效率 | 高 | 低(加锁开销) |
| 出现版本 | JDK 1.2 | JDK 1.0 |
| 使用场景 | 单线程 | 多线程(但推荐用 CopyOnWriteArrayList) |
为什么不用 Vector? 性能太差,所有方法都加锁。多线程场景推荐用
Collections.synchronizedList或CopyOnWriteArrayList。
六、三者综合对比
6.1 核心对比表
| 对比项 | ArrayList | LinkedList | Vector |
|---|---|---|---|
| 底层结构 | Object 数组 | 双向链表 | Object 数组 |
| 随机访问 | ✅ O(1) | ❌ O(n) | ✅ O(1) |
| 头部插入 | ❌ O(n) | ✅ O(1) | ❌ O(n) |
| 尾部插入 | ✅ O(1)(扩容时O(n)) | ✅ O(1) | ✅ O(1) |
| 中间插入 | ❌ O(n) | ⚠️ O(n)(要先找位置) | ❌ O(n) |
| 内存占用 | 少(数组连续) | 多(节点+两个指针) | 少 |
| 线程安全 | ❌ | ❌ | ✅(synchronized) |
| 扩容机制 | 1.5倍 | 不需要扩容 | 2倍 |
| 适用场景 | 查多增少、随机访问 | 头尾增删多、当队列/栈用 | 已废弃 |
6.2 怎么选?
选 List 实现类
↓
需要线程安全吗?
├── 需要 → Collections.synchronizedList 或 CopyOnWriteArrayList
└── 不需要
├── 查多、随机访问多 → ArrayList(绝大多数情况用它)
├── 头尾增删多、需要队列/栈 → LinkedList
└── 都不确定 → 先选 ArrayList
6.3 遍历方式对比
| 遍历方式 | ArrayList | LinkedList |
|---|---|---|
| 普通 for(get(i)) | ✅ 最快(随机访问) | ❌ 极慢(每次都从头/尾找) |
| 增强 for(foreach) | ✅ 快 | ✅ 快(迭代器顺序访问) |
| 迭代器 Iterator | ✅ 快 | ✅ 快 |
| forEach(Lambda) | ✅ 快 | ✅ 快 |
结论:
- ArrayList 用普通 for 最快
- LinkedList 千万不要用普通 for 遍历!用迭代器或增强 for
七、fail-fast 机制(快速失败)
ArrayList、LinkedList、HashMap 等都有 fail-fast 机制。
是什么: 遍历集合时,如果集合结构被修改了(add/remove/clear),会立刻抛出 ConcurrentModificationException。
原理:
java
// modCount:记录集合被修改的次数
protected transient int modCount = 0;
// 每次 add/remove 都会 modCount++
public boolean add(E e) {
ensureCapacityInternal(size + 1);
modCount++; // 修改次数+1
elementData[size++] = e;
return true;
}
// 迭代器初始化时记录 expectedModCount = modCount
// 每次 next() 时检查:if (modCount != expectedModCount) throw ...
注意:
- 迭代器自己的 remove() 方法不会触发异常(它会同步更新 expectedModCount)
- 并发修改才会触发,单线程用集合的 remove 也会触发
- 这不是并发安全保证,只是一种"错误检测机制"
八、思维导图速览
List 三剑客
├── ArrayList(数组实现,最常用)
│ ├── 底层:Object[] 数组
│ ├── 默认容量:10(JDK8懒加载)
│ ├── 扩容:1.5 倍(Arrays.copyOf)
│ ├── 查询 O(1)、增删 O(n)
│ ├── 支持随机访问(RandomAccess)
│ └── 线程不安全
├── LinkedList(双向链表)
│ ├── 底层:双向链表(Node节点 prev+next+item)
│ ├── 不需要扩容
│ ├── 头尾增删 O(1)、中间增删 O(n)(要先找位置)
│ ├── 查询 O(n)(折半查找优化)
│ ├── 实现了 Deque,可当队列/栈
│ └── 线程不安全
├── Vector(已废弃)
│ ├── 底层:Object[] 数组
│ ├── 所有方法 synchronized
│ ├── 扩容 2 倍
│ └── 效率低,不推荐使用
├── 扩容机制(ArrayList)
│ ├── 默认初始容量:10
│ ├── 扩容倍数:1.5 倍
│ ├── 扩容方式:Arrays.copyOf → System.arraycopy
│ └── 优化:预估大小,指定初始容量
├── 遍历方式
│ ├── ArrayList → 普通 for 最快
│ └── LinkedList → 不能用普通 for,用迭代器/增强for
└── fail-fast 机制
├── 并发修改抛 ConcurrentModificationException
└── modCount 记录修改次数
九、写在最后
- ArrayList 是重点中的重点:日常开发 90% 用它,扩容机制面试必问
- LinkedList 了解特点和适用场景:知道它什么时候快什么时候慢,不要被"增删快"的口号忽悠(中间增删并不快)
- Vector 知道为什么被淘汰:全方法加锁效率低,有更好的替代品
- 源码值得看:ArrayList 的 grow()、LinkedList 的 node() 方法都不长,建议读一读
面试高频排序
- ArrayList 和 LinkedList 的区别(必问)
- ArrayList 扩容机制(必问)
- ArrayList 和 Vector 的区别
- ArrayList 遍历方式及性能对比
- fail-fast 机制