当前位置: 首页 > news >正文

【集合框架List进阶】

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

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


一、List 接口概览

ListCollection 的子接口,核心特性:

  • ✅ 元素有序(按插入顺序)
  • ✅ 允许重复元素
  • ✅ 支持通过索引访问(get(index)
  • ✅ 提供插入、删除、查找等操作
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 倍
    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)
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock(); // 写操作加锁try {Object[] elements = getArray();int len = elements.length;// 复制新数组 +1Object[] 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写时复制机制允许并发修改

示例代码:

// ✅ 正确:普通 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,不一致则抛异常。


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

实现类删除方法时间复杂度说明
ArrayListremove(int index)O(n)删除后需移动后续元素
LinkedListremove(int index)O(n)需先遍历到位置,再修改指针
CopyOnWriteArrayListremove(int index)O(n)复制新数组,写时复制

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

优化建议:

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

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

ArrayListadd() 方法源码:

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()

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 注意:遍历时仍需手动同步
synchronized (syncList) {for (String s : syncList) {// ...}
}

方法 2:使用 CopyOnWriteArrayList

List<String> cowList = new CopyOnWriteArrayList<>();
// 读操作无需同步,写操作自动线程安全

方法 3:使用 Vector(不推荐)

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,只有“最合适”的场景选择。

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

http://www.dtcms.com/a/334873.html

相关文章:

  • 【UHD】vivado 2021.1 编译
  • 选择式与生成式超启发算法总结
  • 模型训练监控:TensorBoard与Weights Biases (WB) 使用详解
  • CVE-2024-28752漏洞复现
  • 电子电气架构 --- 软件项目配置管理
  • 序列晋升7:架构原则三十诫
  • 内网穿透实战笔记 1panel 面板部署 frps,Windows 部署 frpc
  • 程序设计|C语言教学——C语言基础3:函数、数组、指针
  • Python虚拟环境与包管理工具(uv、Conda)
  • 一汽红旗7月销量37324辆 同比增长21.1%
  • B站 韩顺平 笔记 (Day 20)
  • P2169 正则表达式
  • 如何运用好DeepSeek为自己服务:智能增强的范式革命 1.1 认知增强的三次浪潮
  • 项目管理进阶——解读大型IT系统集成项目实施要点培训【附全文阅读】
  • GLM-4-Flash:智谱AI推出的首个免费API服务,支持128K上下文
  • 制作 Windows 11 启动U盘
  • Redis缓存
  • Win11和Win10共享打印机提示709用添加Windows凭据来解决的小方法
  • select、poll 和 epoll
  • Python入门第5课:如何定义和使用函数,提升代码复用性
  • Jenkins Pipeline中参数化构建
  • 【wmi异常】关于taskkill命令提示“错误:找不到” 以及无法正常获取设备机器码的处理办法
  • 读书是一场最低成本的高级成长
  • 嵌入式硬件篇---运算放大器
  • OpenCV 图像处理基础操作指南(二)
  • NetBIOS 设置
  • MySQL的索引优化与查询优化:
  • AI搜索引擎下的内容优化新范式:GEO的关键技术解析
  • 12V电压控制小板
  • EXTI配置流程