目录
- [🟡 19 集合框架:List------ArrayList与LinkedList深度对比](#🟡 19 集合框架:List——ArrayList与LinkedList深度对比)
-
- [📑 目录](#📑 目录)
- 一、集合框架概述
-
- [1.1 为什么需要集合框架?](#1.1 为什么需要集合框架?)
- [1.2 集合框架体系结构](#1.2 集合框架体系结构)
- 二、List接口规范
-
- [2.1 List接口核心方法](#2.1 List接口核心方法)
- [2.2 List接口的特性](#2.2 List接口的特性)
- 三、ArrayList源码深度分析
-
- [3.1 基本使用](#3.1 基本使用)
- [3.2 底层数据结构](#3.2 底层数据结构)
- [3.3 扩容机制(核心源码分析)](#3.3 扩容机制(核心源码分析))
- [3.4 性能分析](#3.4 性能分析)
- [3.5 ArrayList的subList陷阱](#3.5 ArrayList的subList陷阱)
- 四、LinkedList源码深度分析
-
- [4.1 基本使用](#4.1 基本使用)
- [4.2 底层数据结构](#4.2 底层数据结构)
- [4.3 核心操作源码](#4.3 核心操作源码)
- [4.4 性能分析](#4.4 性能分析)
- [五、ArrayList vs LinkedList 全面对比](#五、ArrayList vs LinkedList 全面对比)
-
- [5.1 核心对比表](#5.1 核心对比表)
- [5.2 性能基准测试](#5.2 性能基准测试)
- [5.3 内存占用对比](#5.3 内存占用对比)
- 六、Vector与Stack------古老遗迹
-
- [6.1 Vector](#6.1 Vector)
- [6.2 替代方案](#6.2 替代方案)
- 七、使用场景与选型指南
-
- [7.1 选型决策树](#7.1 选型决策树)
- [7.2 实战场景示例](#7.2 实战场景示例)
- 八、常见面试题解析
- 九、综合实战练习
- 十、总结与下篇预告
-
- 本篇核心要点
- [🤔 互动问题](#🤔 互动问题)
- [📖 下篇预告](#📖 下篇预告)
- 参考资料
🟡 19 集合框架:List------ArrayList与LinkedList深度对比
更新日期 :2026年5月 | Java入门到精通系列 · 第三阶段·核心进阶
© 版权声明:本文为原创技术文章,转载请联系作者并注明出处。
📑 目录
- 一、集合框架概述
- 二、List接口规范
- 三、ArrayList源码深度分析
- 四、LinkedList源码深度分析
- [五、ArrayList vs LinkedList 全面对比](#五、ArrayList vs LinkedList 全面对比)
- 六、Vector与Stack------古老遗迹
- 七、使用场景与选型指南
- 八、常见面试题解析
- 九、综合实战练习
- 十、总结与下篇预告
一、集合框架概述
Java集合框架(Java Collections Framework, JCF)是Java语言中最常用的基础设施之一。在上一阶段的学习中,我们已经掌握了数组的使用,但数组有其固有的局限性------长度固定。集合框架正是为了解决这个问题而诞生的。
1.1 为什么需要集合框架?
java
// 数组的局限性
String[] names = new String[3];
names[0] = "Alice";
names[1] = "Bob";
names[2] = "Charlie";
// 如果需要添加第4个元素呢?必须创建新数组!
集合框架的优势:
- 动态扩容:无需关心容量问题
- 丰富的API:排序、查找、遍历一应俱全
- 类型安全:配合泛型使用,编译期检查类型
- 线程安全可选:提供同步版本
1.2 集合框架体系结构
Iterable
│
Collection
/ | \
List Set Queue
/ \ | \
ArrayList HashSet PriorityQueue
LinkedList TreeSet ArrayDeque
Vector
二、List接口规范
List 是一个有序集合(也称为序列)。用户可以精确控制每个元素的插入位置,也可以通过整数索引访问元素。
2.1 List接口核心方法
java
public interface List<E> extends Collection<E> {
// 添加元素
boolean add(E e);
void add(int index, E element);
boolean addAll(Collection<? extends E> c);
// 获取元素
E get(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
// 修改元素
E set(int index, E element);
// 删除元素
E remove(int index);
boolean remove(Object o);
// 查询
int size();
boolean isEmpty();
boolean contains(Object o);
// 遍历
Iterator<E> iterator();
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
// 子列表
List<E> subList(int fromIndex, int toIndex);
}
2.2 List接口的特性
| 特性 | 说明 |
|---|---|
| 有序性 | 元素按插入顺序排列,索引从0开始 |
| 可重复 | 允许存储重复元素 |
| 允许null | 可以存储null值(大多数实现) |
| 随机访问 | 支持通过索引直接访问 |
| 线程不安全 | 默认实现都不是线程安全的 |
三、ArrayList源码深度分析
ArrayList 是List接口最常用的实现类,底层基于动态数组实现。
3.1 基本使用
java
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo {
public static void main(String[] args) {
// 创建ArrayList
List<String> list = new ArrayList<>();
// 添加元素
list.add("Java");
list.add("Python");
list.add("Go");
list.add(1, "Rust"); // 在索引1处插入
// 遍历
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 增强for循环
for (String lang : list) {
System.out.println(lang);
}
}
}
3.2 底层数据结构
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组(用于空实例)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储元素的数组
transient Object[] elementData;
// 实际元素个数
private int size;
}
3.3 扩容机制(核心源码分析)
java
// 添加元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量
elementData[size++] = e;
return true;
}
// 确保容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 修改次数(用于fail-fast)
if (minCapacity - elementData.length > 0) {
grow(minCapacity); // 扩容
}
}
// 核心扩容方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// 复制到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容流程图:
add() → ensureCapacityInternal() → ensureExplicitCapacity() → grow()
↓
newCapacity = old + old/2 (1.5倍)
↓
Arrays.copyOf() → 创建新数组并复制
3.4 性能分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
get(index) |
O(1) | 数组直接索引 |
add(e) (末尾) |
O(1) 平均 | 偶尔触发扩容时O(n) |
add(index, e) |
O(n) | 需要移动元素 |
remove(index) |
O(n) | 需要移动元素 |
contains(e) |
O(n) | 需要遍历 |
size() |
O(1) | 直接返回size字段 |
3.5 ArrayList的subList陷阱
java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D", "E"));
List<String> sub = list.subList(1, 3); // [B, C]
// 陷阱1:subList返回的是视图,不是新列表
sub.set(0, "X");
System.out.println(list); // [A, X, C, D, E] 原列表也被修改了!
// 陷阱2:修改原列表后再操作subList会抛ConcurrentModificationException
list.add("F");
try {
sub.get(0); // 抛出异常!
} catch (ConcurrentModificationException e) {
System.out.println("原列表已修改,subList失效");
}
// 正确做法:创建新的副本
List<String> safeSub = new ArrayList<>(list.subList(1, 3));
四、LinkedList源码深度分析
LinkedList 底层基于双向链表 实现,同时实现了 List 和 Deque 接口。
4.1 基本使用
java
import java.util.LinkedList;
public class LinkedListDemo {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
// 作为List使用
linkedList.add("A");
linkedList.add("B");
linkedList.addFirst("Start");
linkedList.addLast("End");
// 作为Deque(双端队列)使用
linkedList.push("Top"); // 等同于addFirst
linkedList.offer("Bottom"); // 等同于offerLast
linkedList.poll(); // 弹出头部元素
// 作为栈使用
linkedList.push("Stack1");
linkedList.push("Stack2");
System.out.println(linkedList.pop()); // Stack2
System.out.println(linkedList.peek()); // Stack1
}
}
4.2 底层数据结构
java
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
transient int size = 0;
transient Node<E> first; // 头节点
transient Node<E> last; // 尾节点
// 内部节点类
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;
}
}
}
双向链表结构:
null ← [A] ⇄ [B] ⇄ [C] ⇄ [D] → null
↑ ↑
first last
4.3 核心操作源码
java
// 在头部插入
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null) {
last = newNode;
} else {
f.prev = newNode;
}
size++;
modCount++;
}
// 在尾部插入
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null) {
first = newNode;
} else {
l.next = newNode;
}
size++;
modCount++;
}
// 根据索引查找节点(优化:从近的一端开始)
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;
}
}
4.4 性能分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
get(index) |
O(n) | 需要从头/尾遍历 |
add(e) (末尾) |
O(1) | 直接操作last节点 |
addFirst(e) |
O(1) | 直接操作first节点 |
add(index, e) |
O(n) | 需要先定位 |
remove(index) |
O(n) | 定位 + 修改指针 |
contains(e) |
O(n) | 需要遍历 |
size() |
O(1) | 直接返回size字段 |
五、ArrayList vs LinkedList 全面对比
5.1 核心对比表
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 内存占用 | 紧凑,可能浪费尾部空间 | 每个节点额外存储两个指针 |
| 随机访问 | ⚡ O(1) | O(n) |
| 头部插入/删除 | O(n) | ⚡ O(1) |
| 尾部插入 | ⚡ O(1) 平均 | ⚡ O(1) |
| 中间插入/删除 | O(n) | O(n)(定位O(n)+插入O(1)) |
| CPU缓存友好 | ⭐ 友好(连续内存) | 不友好(分散内存) |
| 实现了Deque接口 | ❌ | ✅ |
5.2 性能基准测试
java
import java.util.*;
public class PerformanceTest {
public static void main(String[] args) {
int n = 100000;
// 测试尾部添加
ArrayList<Integer> arrayList = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
arrayList.add(i);
}
System.out.println("ArrayList尾部添加: " + (System.currentTimeMillis() - start) + "ms");
LinkedList<Integer> linkedList = new LinkedList<>();
start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
linkedList.add(i);
}
System.out.println("LinkedList尾部添加: " + (System.currentTimeMillis() - start) + "ms");
// 测试头部添加
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
arrayList.add(0, i);
}
System.out.println("ArrayList头部添加10000次: " + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkedList.add(0, i);
}
System.out.println("LinkedList头部添加10000次: " + (System.currentTimeMillis() - start) + "ms");
// 测试随机访问
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
arrayList.get(new Random().nextInt(arrayList.size()));
}
System.out.println("ArrayList随机访问10000次: " + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkedList.get(new Random().nextInt(linkedList.size()));
}
System.out.println("LinkedList随机访问10000次: " + (System.currentTimeMillis() - start) + "ms");
}
}
典型输出结果:
ArrayList尾部添加: 12ms
LinkedList尾部添加: 25ms
ArrayList头部添加10000次: 15ms
LinkedList头部添加10000次: 1ms
ArrayList随机访问10000次: 1ms
LinkedList随机访问10000次: 320ms
5.3 内存占用对比
java
// ArrayList:元素引用数组 + 少量预留空间
// 假设存储100个Integer对象
// ArrayList额外开销:数组头(16字节) + 预留空间引用 ≈ 几十字节
// LinkedList:每个元素需要一个Node对象
// 每个Node:对象头(16字节) + item引用(4字节) + prev引用(4字节) + next引用(4字节) = 28字节
// 100个元素的额外开销:28 * 100 = 2800字节
六、Vector与Stack------古老遗迹
6.1 Vector
java
// Vector是线程安全的ArrayList(已不推荐使用)
Vector<String> vector = new Vector<>();
vector.add("Hello");
// 每个方法都加了synchronized
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
6.2 替代方案
java
// 如果需要线程安全的List,推荐:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 或者使用CopyOnWriteArrayList(读多写少场景)
import java.util.concurrent.CopyOnWriteArrayList;
List<String> cowList = new CopyOnWriteArrayList<>();
七、使用场景与选型指南
7.1 选型决策树
需要List集合?
│
├── 需要频繁随机访问?
│ └── 是 → ArrayList ✅
│
├── 需要频繁在头部插入/删除?
│ └── 是 → LinkedList ✅
│
├── 需要当作队列/双端队列使用?
│ └── 是 → LinkedList(或ArrayDeque)✅
│
├── 元素数量固定或极少修改?
│ └── 是 → List.of()(不可变列表)✅
│
└── 不确定 → 默认选择ArrayList ✅
7.2 实战场景示例
java
// 场景1:商品列表(频繁随机访问)
public class ProductListDemo {
private List<Product> products = new ArrayList<>();
public Product getProduct(int index) {
return products.get(index); // O(1) 随机访问
}
public void addProduct(Product product) {
products.add(product); // 尾部添加
}
}
// 场景2:最近浏览记录(频繁头部插入,定期删除旧记录)
public class RecentViewDemo {
private LinkedList<String> recentViews = new LinkedList<>();
private static final int MAX_SIZE = 20;
public void view(String itemId) {
recentViews.addFirst(itemId); // O(1) 头部插入
if (recentViews.size() > MAX_SIZE) {
recentViews.removeLast(); // O(1) 尾部删除
}
}
public List<String> getRecentViews() {
return new ArrayList<>(recentViews); // 返回副本
}
}
// 场景3:实现LRU缓存
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // accessOrder=true
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
八、常见面试题解析
面试题1:ArrayList和LinkedList的区别?
标准回答:
- 底层数据结构:ArrayList基于动态数组,LinkedList基于双向链表
- 随机访问:ArrayList支持O(1)随机访问,LinkedList需要O(n)
- 插入删除:头部操作LinkedList更优,尾部操作两者接近
- 内存:ArrayList更紧凑,LinkedList每个元素额外存储两个指针
- 实际使用:绝大多数场景ArrayList性能更好(CPU缓存友好)
面试题2:ArrayList扩容机制?
标准回答:
- 默认初始容量10
- 当容量不足时,扩容为原来的1.5倍(
oldCapacity + (oldCapacity >> 1)) - 通过
Arrays.copyOf()将旧数组内容复制到新数组 - 如果预知数据量,建议使用
new ArrayList<>(capacity)指定初始容量
面试题3:为什么ArrayList的elementData用transient修饰?
java
transient Object[] elementData;
因为ArrayList的实际元素个数size可能小于数组长度。如果直接序列化整个数组,会浪费空间。ArrayList自定义了序列化方法:
java
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
int expectedModCount = modCount;
s.defaultWriteObject();
s.writeInt(size);
for (int i = 0; i < size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
九、综合实战练习
练习1:实现简易ArrayList
java
public class MyArrayList<E> {
private Object[] elements;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public MyArrayList() {
this.elements = new Object[DEFAULT_CAPACITY];
}
public MyArrayList(int capacity) {
this.elements = new Object[capacity];
}
public void add(E e) {
ensureCapacity();
elements[size++] = e;
}
@SuppressWarnings("unchecked")
public E get(int index) {
checkIndex(index);
return (E) elements[index];
}
public E remove(int index) {
checkIndex(index);
E oldValue = get(index);
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elements, index + 1, elements, index, numMoved);
}
elements[--size] = null;
return oldValue;
}
public int size() {
return size;
}
private void ensureCapacity() {
if (size == elements.length) {
int newCapacity = elements.length + (elements.length >> 1);
Object[] newElements = new Object[newCapacity];
System.arraycopy(elements, 0, newElements, 0, size);
elements = newElements;
}
}
private void checkIndex(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
}
}
练习2:使用List实现扑克牌发牌
java
import java.util.*;
public class PokerGame {
private List<Card> deck = new ArrayList<>();
private Map<String, List<Card>> players = new HashMap<>();
public PokerGame(String... playerNames) {
// 初始化牌组
String[] suits = {"♠", "♥", "♦", "♣"};
String[] ranks = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
for (String suit : suits) {
for (String rank : ranks) {
deck.add(new Card(suit, rank));
}
}
// 初始化玩家
for (String name : playerNames) {
players.put(name, new ArrayList<>());
}
}
public void shuffleAndDeal() {
Collections.shuffle(deck); // 洗牌
Iterator<Card> it = deck.iterator();
String[] names = players.keySet().toArray(new String[0]);
int playerIndex = 0;
while (it.hasNext()) {
players.get(names[playerIndex % names.length]).add(it.next());
playerIndex++;
}
}
public void showHands() {
for (Map.Entry<String, List<Card>> entry : players.entrySet()) {
System.out.println(entry.getKey() + " 的手牌: " + entry.getValue());
}
}
static class Card {
String suit, rank;
Card(String suit, String rank) {
this.suit = suit;
this.rank = rank;
}
@Override
public String toString() { return suit + rank; }
}
public static void main(String[] args) {
PokerGame game = new PokerGame("Alice", "Bob", "Charlie");
game.shuffleAndDeal();
game.showHands();
}
}
十、总结与下篇预告
本篇核心要点
| 要点 | 说明 |
|---|---|
| ArrayList底层 | 动态数组,扩容1.5倍 |
| LinkedList底层 | 双向链表,额外实现Deque |
| 随机访问 | ArrayList O(1) vs LinkedList O(n) |
| 默认选择 | 绝大多数场景选ArrayList |
| 内存考虑 | LinkedList每个元素多8-16字节指针开销 |
| CPU缓存 | ArrayList连续内存,CPU缓存命中率高 |
🤔 互动问题
- ArrayList的扩容倍数为什么选择1.5而不是2?
- LinkedList的
node()方法是如何优化查找性能的? - 如果你要实现一个消息队列,应该选择ArrayList还是LinkedList?为什么?
📖 下篇预告
下一篇我们将学习**《集合框架:Set与Map》**,深入探讨HashSet、TreeSet、HashMap、TreeMap的底层实现,包括哈希原理和红黑树的基本概念。