首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【集合框架List进阶】

【集合框架List进阶】

作者头像
艾伦耶格尔
发布2025-08-28 15:50:44
发布2025-08-28 15:50:44
10600
代码可运行
举报
文章被收录于专栏:Java基础Java基础
运行总次数:0
代码可运行

在 Java 开发中,List 是最常用的集合接口之一,它允许存储有序、可重复的元素。JDK 提供了多种 List 实现,每种都有其特定的性能特征和适用场景。

本文将深入剖析 Java 中常见的 List 实现类,解答面试高频问题,帮助你掌握 “何时用哪种 List” 的核心决策能力。


一、List 接口概览

ListCollection 的子接口,核心特性:

  • ✅ 元素有序(按插入顺序)
  • ✅ 允许重复元素
  • ✅ 支持通过索引访问(get(index)
  • ✅ 提供插入、删除、查找等操作
代码语言:javascript
代码运行次数:0
运行
复制
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
System.out.println(list.get(0)); // 输出 A

二、常见 List 实现类对比

实现类

底层结构

线程安全

随机访问

插入/删除

适用场景

ArrayList

动态数组

O(1)

尾部 O(1),中间 O(n)

频繁读取、尾部增删

LinkedList

双向链表

O(n)

任意位置 O(1)(已定位)

频繁中间插入删除

Vector

动态数组

✅(方法同步)

O(1)

尾部 O(1),中间 O(n)

老旧系统,不推荐新项目

CopyOnWriteArrayList

写时复制数组

O(1)

O(n)(写操作)

读多写少的并发场景


三、核心实现类详解

1. ArrayList:基于动态数组
特点:
  • 随机访问快:通过索引直接定位,时间复杂度 O(1)
  • 尾部操作高效:在末尾添加/删除元素,时间复杂度 O(1)
  • 中间操作慢:插入或删除需移动后续元素,O(n)
  • 内存连续:占用连续内存空间,缓存友好
扩容机制:

初始容量:10(无参构造)

扩容策略:原容量的 1.5 倍

代码语言:javascript
代码运行次数:0
运行
复制
int newCapacity = oldCapacity + (oldCapacity >> 1); // 位移优化性能

扩容步骤:

  1. 检查是否需要扩容
  2. 创建新数组(1.5倍)
  3. 复制原数组元素
  4. 更新引用

💡 建议:若已知数据量,使用 new ArrayList<>(initialCapacity) 避免频繁扩容。


2. LinkedList:基于双向链表
特点:
  • 插入/删除快:只需修改前后节点指针,O(1)(前提是已定位到位置)
  • 随机访问慢:需从头或尾遍历,O(n)
  • 内存非连续:每个节点包含 prevnext 指针,内存开销大
  • 支持双端操作:天然适合实现栈(push/pop)和队列(offer/poll
注意:
  • list.remove(index) 仍需 O(n) 时间遍历到位置
  • 真正高效的是 addFirst()addLast()removeFirst() 等操作

误区澄清: “LinkedList 在任意位置插入都是 O(1)” 是错误说法。 正确说法:定位到节点后,插入是 O(1),但定位本身是 O(n)。


3. Vector:线程安全的 ArrayList
特点:
  • 与 ArrayList 类似,但方法用 synchronized 修饰
  • 扩容策略:翻倍扩容(原容量 × 2)
  • 性能较差:同步带来额外开销
  • 不推荐使用:现代并发编程应使用 CopyOnWriteArrayList 或显式同步

4. CopyOnWriteArrayList:读写分离的并发 List
核心机制:写时复制(Copy-On-Write)
代码语言:javascript
代码运行次数:0
运行
复制
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock(); // 写操作加锁
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 复制新数组 +1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements); // 原子替换引用
        return true;
    } finally {
        lock.unlock();
    }
}
特点:
  • ✅ 读操作无锁get() 不加锁,性能极高
  • ✅ 写操作安全:加 ReentrantLock,保证线程安全
  • ✅ 最终一致性:读可能看到旧数据(适合事件监听、配置管理)
  • ❌ 写性能低:每次写都复制数组,O(n)
  • ❌ 内存占用高:存在多个数组副本

适用场景:读远多于写的并发环境,如:

  • 监听器列表
  • 缓存配置
  • 白名单/黑名单

四、List 遍历过程中能否修改?(高频面试题)

答案:取决于遍历方式!

遍历方式

是否可修改

说明

普通 for 循环

直接通过 set(index, value) 修改

增强 for 循环(foreach)

底层使用 Iterator,会抛 ConcurrentModificationException

Iterator 迭代器

✅(有限制)

只能用 iterator.remove() 或 iterator.set()

CopyOnWriteArrayList + foreach

写时复制机制允许并发修改

示例代码:
代码语言:javascript
代码运行次数:0
运行
复制
// ✅ 正确:普通 for 循环修改
for (int i = 0; i < list.size(); i++) {
    list.set(i, list.get(i) * 2);
}

// ❌ 错误:foreach 中直接修改
for (Integer num : list) {
    list.remove(num); // 抛 ConcurrentModificationException
}

// ✅ 正确:使用 Iterator
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    Integer num = it.next();
    if (num == 2) {
        it.remove(); // 使用迭代器的 remove
        // it.set(4); // 修改值
    }
}

💡 原理ArrayList 等集合使用 modCount 记录结构修改次数,迭代器会检查 expectedModCount,不一致则抛异常。


五、如何快速删除指定下标的元素?

实现类

删除方法

时间复杂度

说明

ArrayList

remove(int index)

O(n)

删除后需移动后续元素

LinkedList

remove(int index)

O(n)

需先遍历到位置,再修改指针

CopyOnWriteArrayList

remove(int index)

O(n)

复制新数组,写时复制

⚠️ 注意:即使是 LinkedListremove(index) 也不是 O(1),因为需要遍历定位。

优化建议:
  • 若频繁删除首尾元素:使用 LinkedList 的 removeFirst() / removeLast()
  • 若已知节点引用:LinkedList 可通过 remove(Node) 实现 O(1)

六、ArrayList 为什么不是线程安全的?(深入源码)

ArrayListadd() 方法源码:

代码语言:javascript
代码运行次数:0
运行
复制
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // ① 检查扩容
    elementData[size++] = e;           // ② 赋值 + size 自增
    return true;
}

在多线程环境下,可能出现以下问题:

1. 元素为 null
  • 线程 A 执行到 elementData[size] = e,但未执行 size++
  • 线程 B 也执行 elementData[size] = e(此时 size 未变)
  • 两个线程都写入同一个位置,后写的覆盖前写的,导致某个元素丢失,后续位置为 null
2. 索引越界
  • 线程 A 判断 size=9,容量=10,无需扩容
  • 线程 B 同样判断无需扩容
  • 线程 A 执行 size++ 后 size=10
  • 线程 B 执行 elementData[10] = e → 数组越界!
3. size 不一致
  • size++ 不是原子操作(读取 → +1 → 写回)
  • 两个线程同时读取 size=5,都执行 size=6,最终只加了1次

七、如何让 ArrayList 线程安全?

方法 1:使用 Collections.synchronizedList()
代码语言:javascript
代码运行次数:0
运行
复制
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 注意:遍历时仍需手动同步
synchronized (syncList) {
    for (String s : syncList) {
        // ...
    }
}
方法 2:使用 CopyOnWriteArrayList
代码语言:javascript
代码运行次数:0
运行
复制
List<String> cowList = new CopyOnWriteArrayList<>();
// 读操作无需同步,写操作自动线程安全
方法 3:使用 Vector(不推荐)
代码语言:javascript
代码运行次数:0
运行
复制
List<String> vector = new Vector<>();

推荐顺序CopyOnWriteArrayList > Collections.synchronizedList() > Vector


八、ArrayList vs LinkedList:如何选择?

场景

推荐实现

频繁按索引访问、遍历

✅ ArrayList

频繁在列表末尾添加/删除

✅ ArrayList

频繁在列表中间插入/删除

✅ LinkedList(但需评估是否真需要)

实现栈或队列

✅ LinkedList(或 ArrayDeque)

并发读多写少

✅ CopyOnWriteArrayList

单线程高性能访问

✅ ArrayList

💡 实际建议:大多数场景优先使用 ArrayList,除非有明确的中间插入删除需求。


九、常见问题

  1. ArrayList 扩容机制? → 1.5 倍扩容,使用 (oldCapacity + oldCapacity >> 1) 优化性能。之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。
  2. LinkedList 是双向链表吗? → 是,Node 包含 prevnext 指针。
  3. CopyOnWriteArrayList 为什么读不加锁? → 写时复制 + volatile 数组引用,保证可见性。
  4. 遍历 List 时删除元素如何避免异常? → 使用 Iterator.remove()CopyOnWriteArrayList
  5. ArrayList 和 Vector 的区别?Vector 方法同步,扩容翻倍,性能差,已过时。

结语

List 是 Java 集合体系的基石,理解其不同实现的底层结构、性能特征和线程安全机制,是每个 Java 开发者的必备技能。

🔑 核心原则:没有“最好”的 List,只有“最合适”的场景选择。

掌握这些知识,不仅能写出高性能代码,也能在面试中从容应对各种“为什么”和“如何选择”的问题。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、List 接口概览
  • 二、常见 List 实现类对比
  • 三、核心实现类详解
    • 1. ArrayList:基于动态数组
      • 特点:
      • 扩容机制:
    • 2. LinkedList:基于双向链表
      • 特点:
      • 注意:
    • 3. Vector:线程安全的 ArrayList
      • 特点:
    • 4. CopyOnWriteArrayList:读写分离的并发 List
      • 核心机制:写时复制(Copy-On-Write)
      • 特点:
  • 四、List 遍历过程中能否修改?(高频面试题)
    • 示例代码:
  • 五、如何快速删除指定下标的元素?
    • 优化建议:
  • 六、ArrayList 为什么不是线程安全的?(深入源码)
    • 1. 元素为 null
    • 2. 索引越界
    • 3. size 不一致
  • 七、如何让 ArrayList 线程安全?
    • 方法 1:使用 Collections.synchronizedList()
    • 方法 2:使用 CopyOnWriteArrayList
    • 方法 3:使用 Vector(不推荐)
  • 八、ArrayList vs LinkedList:如何选择?
  • 九、常见问题
  • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档