List梳理

一、 List 家族全景图

首先,你要清楚 List 接口的核心特点:有序(存储顺序和插入顺序一致)、可重复、支持索引访问。

主要实现类如下:

ArrayList:动态数组(面试绝对核心,90% 的场景都用它)。

LinkedList:双向链表(作为对比陪跑,但需了解其作为队列/栈的特性)。

Vector:古老的线程安全数组(全同步,性能差,已淘汰)。

CopyOnWriteArrayList:JUC 包下的并发 List(读写分离,读多写少场景)。

二、 核心王者:ArrayList (深度剖析)

  1. 底层结构
    结构:Object[] elementData。
    特点:内存连续,支持快速随机访问(Random Access)。
  2. 初始化与懒加载 (JDK 1.8 变化)
    JDK 1.7:new ArrayList() 时,直接创建一个长度为 10 的数组。
    JDK 1.8:new ArrayList() 时,赋值为一个空数组 (DEFAULTCAPACITY_EMPTY_ELEMENTDATA)。
    懒加载:只有当第一次调用 add() 方法时,才真正将数组初始化为默认容量 10。
    目的:节省内存。如果创建了 List 但没用,就不占用空间。
  3. 扩容机制 (Grow)
    当 add 元素导致数组满了,会触发扩容:
    计算新容量:
    java
    复制
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    公式:old + 0.5 * old,即扩容为原来的 1.5 倍。
    对比:Vector 是扩容 2 倍。
    数据迁移:
    创建一个新的大数组。
    调用 System.arraycopy(native 方法,极快)将旧数据复制过去。
    代价:扩容是高成本操作。
    优化建议:如果你知道大概的数据量,请在构造时指定 initialCapacity,避免频繁扩容。
  4. 删除元素 (Remove)
    尾部删除:O(1),非常快。
    中间删除:O(n)。因为删除后,需要把后面的所有元素向前移动一位(填补空缺),依然使用 System.arraycopy。
    三、 链表实现:LinkedList
  5. 底层结构
    结构:双向链表。
    节点:Node 对象,包含 prev (前驱)、next (后继)、item (数据)。
    java
    复制
    private static class Node {
    E item;
    Node next;
    Node prev;
    }
  6. 特性
    内存:不连续。每个节点都需要额外存储两个引用,内存占用比 ArrayList 高。
    访问:不支持随机访问。get(i) 需要从头(或尾)遍历,复杂度 O(n)。
    优化:JDK 做了微小优化,如果 i < size/2,从头找;否则从尾找。
    插入/删除:
    理论上:只要拿到了节点,插入/删除只是修改指针,复杂度 O(1)。
    实际上:如果你要"删除第 i 个元素",你得先遍历找到它,这步是 O(n)。只有在使用 Iterator (存在游标,记录了当前节点的索引,不需要再次遍历查询)遍历过程中删除才是真正的 O(1)。
  7. 额外身份
    LinkedList 实现了 Deque (双端队列) 接口。
    因此它可以用作 栈 (Stack) 或 队列 (Queue)。
    四、 巅峰对决:ArrayList vs LinkedList

    ★ 高级回答点:CPU 缓存局部性 (Locality of Reference)

回答:除了随机访问快,还有一个重要原因是 CPU 缓存友好。

ArrayList 的内存是连续的,CPU 读取时会利用预读机制将相邻数据加载到 L1/L2 缓存中,遍历速度极快。

而 LinkedList 内存分散,遍历时频繁发生 Cache Miss(缓存未命中),导致 CPU 必须频繁去主存取数,性能相差巨大。

五、 线程安全 List

  1. Vector (淘汰)
    所有方法加 synchronized。
    性能太差,扩容是 2 倍。别用。
  2. Collections.synchronizedList (不推荐)
    包装类,使用 mutex 对象锁。性能一般。
  3. CopyOnWriteArrayList (JUC 核心)
    这是并发场景下的首选 List。

核心原理:写时复制 (COW)

读操作:完全不加锁。直接读取原数组,性能极高。

写操作 (add/set):

加锁 (ReentrantLock)。

复制:将原数组复制一份,长度 +1。

修改:在新数组上执行修改。

替换:将引用指向新数组 (setArray)。

解锁。

优点:读写分离,读性能极佳,适合读多写少(如白名单、配置列表)。

缺点:

内存占用:写的时候内存里会有两份数组。

数据最终一致性:写入的数据,读线程可能不会立马读到(因为没切引用前读的还是旧数组),不适合实时性要求极高的场景。

六、 面试避坑指南 (常见陷阱)

  1. Arrays.asList() 的坑
java 复制代码
List<String> list = Arrays.asList("a", "b");
list.add("c"); // 报错!UnsupportedOperationException

原因:它返回的不是 java.util.ArrayList,而是 Arrays 内部的一个静态内部类。

特点:它是固定长度的,没有重写 add/remove 方法。

  1. subList 的坑
java 复制代码
List<String> sub = list.subList(0, 2);

特点:返回的不是新 List,而是原 List 的一个视图 (View)。

坑:

修改 sub 会影响原 list。

修改原 list(结构性修改,如 add/remove),会导致 sub 遍历时抛出 ConcurrentModificationException。

内存泄漏:如果你有一个巨大的 List,截取了一个小的 subList 并且长期持有,那么整个大 List 的数组都无法被 GC 回收(因为 subList 强引用了原数组)。

  1. 循环删除的坑

错误写法

java 复制代码
for (String s : list) {
    if ("a".equals(s)) list.remove(s); // 抛出 ConcurrentModificationException
}

正确写法

java 复制代码
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("a".equals(it.next())) {
        it.remove(); // 必须用迭代器的 remove
    }
}

原理:ArrayList 内部维护了一个 modCount 变量,记录修改次数。迭代器在遍历时会检查 expectedModCount == modCount,如果不一致(外部修改了),就报错(Fail-Fast 机制)。

七、 总结

首选:99% 的情况用 ArrayList。

队列/栈:用 LinkedList 或 ArrayDeque。

并发读多写少:用 CopyOnWriteArrayList。

扩容:ArrayList 扩容 1.5 倍,尽量指定初始容量。

性能:ArrayList 赢在随机访问和 CPU 缓存亲和性。

相关推荐
东木月2 小时前
使用python获取Windows产品标签
开发语言·windows·python
星火开发设计2 小时前
C++ multiset 全面解析与实战指南
开发语言·数据结构·c++·学习·set·知识
小猪咪piggy2 小时前
【leetcode100】回溯
数据结构·算法
星火开发设计3 小时前
C++ stack 全面解析与实战指南
java·数据结构·c++·学习·rpc··知识
C雨后彩虹4 小时前
书籍叠放问题
java·数据结构·算法·华为·面试
Jasmine_llq4 小时前
《UVA11181 条件概率 Probability|Given》
数据结构·算法·深度优先搜索(dfs)·剪枝(可行性剪枝)·组合枚举(递归暴力枚举)·条件概率统计与归一化
小范馆4 小时前
解决 Windows 11 安装时提示 “不支持 TPM 2.0” 和 “不支持安全启动” 的问题
windows·安全
还下着雨ZG4 小时前
【Window技能 01】每天自动关机:使用CMD脚本+任务计划程序实现每天定时关闭计算机
windows
洛水如云5 小时前
重塑数据管理逻辑!文件夹同步的实用指南(含工具选型 + 实操步骤)
windows·microsoft·电脑