List 集合详解!

举报
喵手 发表于 2025/12/08 20:49:48 2025/12/08
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

一、ArrayList:一间会自动扩建的“顺序仓库”

1.1 底层结构

  • 底层是 动态数组Object[] elementData
  • 元素在内存中是 连续存储
  • 支持 随机下标访问,因此 get(index)set(index) 是 O(1)
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

System.out.println(list.get(1)); // B,典型 O(1) 访问

1.2 扩容机制(grow 策略)

看下核心思想就行,不用死记源码细节。

当你 add 一个元素,而内部数组已经满了,就会触发扩容操作:

大致伪代码(基于 JDK 8 思路简化):

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 1.5 倍扩容:newCapacity = old + old/2
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    if (newCapacity < minCapacity) {
        newCapacity = minCapacity;
    }

    // 复制到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

关键点总结:

  1. 扩容比例:约 1.5 倍

  2. 数组拷贝成本:O(n)

    • 每次扩容都要把旧数组的元素复制到新数组里
  3. 均摊复杂度(amortized)

    • 虽然扩容是 O(n),但不是每次 add 都扩容
    • 所以 末尾追加 add 操作平均是 O(1)

1.3 时间复杂度分析(ArrayList)

操作 复杂度 说明
get(index) O(1) 直接按下标访问数组
set(index, element) O(1) 直接覆盖数组元素
add(element)(尾部) 均摊 O(1) 偶尔会 O(n) 扩容
add(index, element) O(n) 需要挪动 index 之后的所有元素
remove(index) O(n) 需要把后面的元素往前搬
contains(element) O(n) 线性遍历

一句话:

ArrayList:读快(随机访问牛),中间插删慢。适合“读多写少且多是尾部追加”的场景。

二、LinkedList:一条会插队的“双向人链”

2.1 底层结构:双向链表

LinkedList 底层是 双向链表,每个节点长这样:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}

特点:

  • 每个节点知道自己的 prevnext
  • 内存不连续,通过引用把节点串起来
  • 头尾插入删除很快,中间插入/删除只要找到节点,改指针即可

简单示例:

LinkedList<String> list = new LinkedList<>();

list.add("A");        // 尾部插入
list.addFirst("0");   // 头部插入 O(1)
list.addLast("B");    // 尾部插入 O(1)

list.removeFirst();   // 头删 O(1)
list.removeLast();    // 尾删 O(1)

2.2 时间复杂度分析(LinkedList)

重点来了:随机访问其实不行

操作 复杂度 说明
get(index) O(n) 需要从头/尾走到 index
addFirst(element) / addLast O(1) 只改头尾指针
removeFirst() / removeLast() O(1) 同上
在“已知节点处”插入/删除(内部方法) O(1) 修改前后指针即可
add(index, element) O(n) 先 O(n) 找到节点,再 O(1) 插入
remove(index) O(n) 同理
contains(element) O(n) 线性遍历

也就是说:

  • 你不知道节点位置,只给 index:它很慢(O(n))
  • 你只在头尾操作:它很快

2.3 ArrayList vs LinkedList 场景对比

我们来搞个“擂台表”:

维度 ArrayList LinkedList
底层结构 动态数组 双向链表
随机访问 get(index) O(1) ✅ 很快 O(n) ❌ 需要遍历
尾部追加 add(element) 均摊 O(1) O(1)(尾插)
中间插入/删除 O(n)(搬运数组元素) O(n) 找到节点,接着 O(1) 插入
头部插入/删除 O(n) O(1) 非常适合
内存局部性 好(数组连续) 差(节点分散)
典型使用场景 查询多、按索引访问多 频繁头尾操作、频繁插入删除

换句话说:

  1. 如果你的逻辑经常 get(i)set(i)、for 循环按下标遍历 —— 优先 ArrayList
  2. 如果你是队列/双端队列,频繁在头尾插入删除 —— 可以考虑 LinkedList(或 ArrayDeque)

三、CopyOnWriteArrayList:读的时候“看旧版”,写的时候“印新版”

现在上场的是偏“并发场景”的大哥:CopyOnWriteArrayList
名字很直白:“写时复制”。

3.1 写时复制机制

核心思想一句话:

读时不加锁,写时复制一个新数组,把修改后的新数组替换旧数组。

简化版伪代码(非真实源码,只讲原理):

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] oldArray = array;
        int len = oldArray.length;
        // 1. 新建一个数组,比原来多 1 个
        Object[] newArray = Arrays.copyOf(oldArray, len + 1);
        // 2. 把新元素放在最后
        newArray[len] = e;
        // 3. 用新数组替换旧的
        array = newArray;
        return true;
    } finally {
        lock.unlock();
    }
}

读取操作:

public E get(int index) {
    return (E) array[index];   // 直接读,不加锁
}

所以特性是:

  • 读:无锁,非常适合高并发读
  • 写:每次都复制整个数组,代价昂贵(O(n) 且创建新数组)

3.2 迭代器的“快照语义”

CopyOnWriteArrayList 的另一个特点是:

它的迭代器遍历的是 创建迭代器那一刻的快照数组

即:

  • 遍历过程中,即使别的线程修改了列表,你也不会 ConcurrentModificationException
  • 你看到的就是某个时间点的“历史版本”

示例代码:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");

for (String s : list) {
    System.out.println(s);
    list.add("C"); // 不会抛 ConcurrentModificationException
}
System.out.println("Size: " + list.size()); // 3 or more,看你加了几次

对比 ArrayList

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");

for (String s : list) {
    System.out.println(s);
    list.add("C"); // 这里会抛 ConcurrentModificationException
}

3.3 时间复杂度与代价

操作 复杂度 说明
get(index) O(1),无锁 从当前数组快照读
遍历 O(n),无锁 基于快照数组
add/remove/set O(n) + 分配数组 复制旧数组并修改
并发读写是否安全 读无锁,写有锁

优点:

  • 读操作极其简单、安全
  • 不会 ConcurrentModificationException
  • 遍历无锁,适合读多写少场景

缺点:

  • 写操作非常重:复制整个数组 + GC 压力
  • 不适合写多、列表很大的场景

3.4 适用场景

一句非常实在的话:

CopyOnWriteArrayList = “读特别多,写很少”的并发场景神器。

常见使用场景:

  • 监听器列表、订阅者列表(偶尔注册/取消,频繁通知)
  • 系统配置列表(启动时初始化,运行中偶尔变更,读取很多)
  • 白名单/黑名单缓存:偶尔更新、频繁检查

示例:

class EventBus {
    private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();

    public void register(Listener l) {
        listeners.add(l); // 写少
    }

    public void publish(Event e) {
        for (Listener l : listeners) {  // 读多
            l.onEvent(e);
        }
    }
}

四、三者总结:到底什么时候选谁?

如果把三者当成“角色卡”,能大概这么选:

  1. ArrayList

    • 主打:随机访问快,遍历快,内存局部性好
    • 场景:大部分是读和尾部追加,比如分页数据、列表展示、缓存数组等
    • 并发:非线程安全,多线程要自己加锁或用 Collections.synchronizedList
  2. LinkedList

    • 主打:头尾插删快
    • 场景:作为 队列/双端队列(Deque),如频繁 addFirst/removeFirst
    • 但在现代 Java 里,很多情况下用 ArrayDeque 会更合适
  3. CopyOnWriteArrayList

    • 主打:并发读多写少,读无锁,遍历安全
    • 场景:监听器列表、配置列表、几乎只读的共享集合
    • 不适合:写操作频繁、集合超大

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。