👉 用 ArrayList 存数据,结果插入时卡住了?
👉 想删除某个元素,却发现索引错乱了?
👉 不知道该用 ArrayList 还是 LinkedList,选错了导致性能瓶颈?
一、List 是什么?—— 有序可重复的“列表”🎯 核心特点有序(Ordered):元素按插入顺序排列允许重复:可以存储多个相同的值支持索引访问:可以通过 get(index) 快速定位元素 ✅ 典型场景:
用户列表展示购物车商品管理日志记录🖼️ 图示:List 的逻辑结构代码语言:javascript代码运行次数:0运行复制+--------+ +--------+ +--------+
| index0 | -> | index1 | -> | index2 | -> ...
+--------+ +--------+ +--------+
| valueA | | valueB | | valueC |
+--------+ +--------+ +--------+ ✅ 核心操作:
添加元素:add(E e)获取元素:get(int index)删除元素:remove(int index)二、List 的核心实现类1. ArrayList —— 基于“动态数组”的实现🎯 核心特性底层是数组:Object[] elementData支持快速随机访问:通过索引直接定位,时间复杂度 O(1)动态扩容:当容量不够时,自动扩展(默认扩容 1.5 倍)非线程安全代码语言:javascript代码运行次数:0运行复制List
list.add("Apple");
list.add("Banana");
String first = list.get(0); // 快速获取第一个元素🔍 动态扩容机制代码语言:javascript代码运行次数:0运行复制private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 旧容量/2 (即 1.5 倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 创建新数组,复制数据
elementData = Arrays.copyOf(elementData, newCapacity);
}内存变化:
代码语言:javascript代码运行次数:0运行复制扩容前:[Apple][Banana] (容量=2, size=2)
扩容后:[Apple][Banana][ ] (容量=3, size=2) ✅ 建议:初始化时指定合理容量,避免频繁扩容影响性能。
❌ 经典误区:中间插入/删除慢代码语言:javascript代码运行次数:0运行复制// 错误:频繁在中间插入/删除,导致后面元素移动,性能 O(n)
list.add(1, "Orange"); // 插入到索引 1
list.remove(1); // 删除索引 1 ✅ 结论:ArrayList 适合查询多、增删少的场景。
2. LinkedList —— 基于“双向链表”的实现🎯 核心特性底层是双向链表:每个节点包含前后指针头尾增删快:时间复杂度 O(1)支持栈/队列操作:实现了 Deque 接口非线程安全代码语言:javascript代码运行次数:0运行复制LinkedList
list.addFirst("Apple"); // 头插
list.addLast("Banana"); // 尾插
String first = list.getFirst(); // 快速获取第一个元素🖼️ 图示:双向链表的内存布局代码语言:javascript代码运行次数:0运行复制地址 2000: +--------+--------+--------+
|prev=null| data=A |next=3000|
+--------+--------+--------+
地址 3000: +--------+--------+--------+
|prev=2000| data=B |next=null|
+--------+--------+--------+逻辑结构:
代码语言:javascript代码运行次数:0运行复制head tail
| |
v v
+--------+ +--------+
|null|A|<--->|A|B|null|
+--------+ +--------+
2000 3000 ✅ 适用场景:频繁在头部或尾部增删元素,如消息队列、栈操作。
❌ 经典误区:随机访问慢代码语言:javascript代码运行次数:0运行复制// 错误:频繁随机访问,导致遍历整个链表,性能 O(n)
String third = list.get(2); // 需要遍历两次才能找到第三个元素 ✅ 结论:LinkedList 适合头尾操作多、随机访问少的场景。
三、List 的常用方法详解1. 添加元素代码语言:javascript代码运行次数:0运行复制// 在末尾添加
list.add("Apple");
// 在指定位置插入
list.add(1, "Banana"); // 插入到索引 12. 获取元素代码语言:javascript代码运行次数:0运行复制// 通过索引获取
String first = list.get(0);
// 获取第一个/最后一个元素
String first = list.get(0);
String last = list.get(list.size() - 1);3. 删除元素代码语言:javascript代码运行次数:0运行复制// 删除指定位置的元素
list.remove(1); // 删除索引 1 的元素
// 删除指定对象(第一次出现)
list.remove("Apple");4. 替换元素代码语言:javascript代码运行次数:0运行复制// 替换指定位置的元素
list.set(1, "Grape"); // 将索引 1 的元素替换为 Grape5. 查找元素代码语言:javascript代码运行次数:0运行复制// 判断是否包含某个元素
boolean contains = list.contains("Apple");
// 查找元素的位置
int index = list.indexOf("Apple"); // 返回第一个 Apple 的索引
int lastIndex = list.lastIndexOf("Apple"); // 返回最后一个 Apple 的索引6. 子列表操作代码语言:javascript代码运行次数:0运行复制// 获取子列表
List
synchronized (syncList) {
syncList.add("Apple");
} ❌ 缺点:
整个 List 对象加锁,粒度过大,性能差。不支持并发读写,可能导致阻塞。2. CopyOnWriteArrayList —— 写时复制的线程安全 List🎯 核心特性写时复制:每次修改(add, remove)都会创建一个新数组副本。适用于读多写少的场景:读操作无锁,写操作加锁但不影响读。代码语言:javascript代码运行次数:0运行复制List
cowList.add("Apple");
cowList.add("Banana"); ✅ 适用场景:
并发读多、写少的场景,如日志记录、事件监听器等。五、高频问题 & 高分回答Q1: ArrayList 和 LinkedList 如何选择? 答:
ArrayList:基于数组,查询快(O(1)),增删慢(O(n));
适合随机访问多、增删少的场景。LinkedList:基于链表,增删快(O(1) 头尾),查询慢(O(n));
适合频繁头尾增删的场景,或实现栈/队列。
我们项目中用户列表用 ArrayList,消息队列用 LinkedList。Q2: ArrayList 的扩容机制是怎样的? 答:
默认容量 10;扩容时,新容量 = 原容量 × 1.5;使用 Arrays.copyOf() 复制数据;扩容是耗时操作,建议初始化时指定合理容量。Q3: LinkedList 的优势和劣势是什么? 答:
优势:头尾增删快(O(1)),支持栈/队列操作;劣势:随机访问慢(O(n)),内存开销稍大(每个节点有前后指针)。
适合频繁头尾操作的场景,如消息队列、栈操作。Q4: CopyOnWriteArrayList 的工作原理? 答:
写时复制:每次修改(add, remove)都会创建一个新数组副本;读操作无锁,写操作加锁但不影响读;适用于读多写少的场景,如日志记录、事件监听器等。六、总结:一张表搞懂 List 的选型场景
推荐实现
关键点
随机访问多、增删少
ArrayList
查询快,扩容注意
头尾增删多、随机访问少
LinkedList
头尾操作极快,随机访问慢
并发读多写少
CopyOnWriteArrayList
写时复制,读无锁
🔚 最后一句话 List 是 Java 集合框架中最基础、最常用的接口之一。
它不仅仅是一个“容器”,更是我们日常开发中处理有序数据的核心工具。
只有当你真正理解了 ArrayList 的扩容机制、LinkedList 的双向链表结构,
以及 CopyOnWriteArrayList 的写时复制原理,
你才能写出高效、健壮、专业的 Java 代码!
希望这篇能帮你彻底搞懂 List 接口及其常见实现!