跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

Java List 集合详解

详细讲解了 Java List 接口的几种常见实现类,包括 ArrayList、LinkedList、Vector 和 CopyOnWriteArrayList。重点分析了它们在底层数据结构、线程安全性、性能表现及扩容机制上的区别。文章还探讨了在遍历过程中修改元素的正确方式,避免并发修改异常,并提供了 ArrayList 转数组、数组转 List 的常用方法及注意事项。最后解释了泛型不支持基本数据类型的原因及自动装箱拆箱机制。

KernelLab发布于 2026/3/21更新于 2026/9/469 浏览

1. List 的几种实现

List 是有序的 Collection,允许元素重复,实现 List 的类有 LinkedList、ArrayList、Vector、Stack 等。

ArrayList是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。

LinkedList顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

Vector是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。

Vector 和 ArrayList 作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。而 LinkedList 进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。

2. List 可以一边遍历一边修改元素吗

具体问题具体分析,取决于遍历方式和具体的 List 实现类。

(1)使用普通 for 循环遍历:可以在遍历过程中修改元素,只要修改的索引不超出 List 的范围即可。

import java.util.ArrayList;
import java.util.List;

public class ListTraversalAndModification {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        // 使用普通 for 循环遍历并修改元素
        for (int i = 0; i < list.size(); i++) {
            list.set(i, list.get(i) * 2);
            System.out.println(list);
        }
    }
}

(2)使用 foreach 循环遍历:一般不建议在 foreach 循环中直接修改正在遍历的 List 元素,因为这可能会导致意外的结果或 ConcurrentModificationException 异常。在 foreach 循环中修改元素可能会破坏迭代器的内部状态,因为 foreach 循环底层是基于迭代器实现的,在遍历过程中修改集合结构,会导致迭代器的预期结构和实际结构不一致。

import java.util.ArrayList;
import java.util.List;

public class ListTraversalAndModification {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        // 使用 foreach 循环遍历并尝试修改元素,会抛出 ConcurrentModificationException 异常
        for (Integer num : list) {
            if (list.indexOf(num) != -1) {
                list.set(list.indexOf(num), num * 2);
            }
            System.out.println(list);
        }
    }
}

(3)使用迭代器遍历时:可以使用迭代器的 remove 方法来删除元素,但如果要替换元素的值,对于不可变对象(如 Integer,String),必须通过 ListIterator 的 set 方法来进行,而不是直接通过 List 的 set 方法,否则会抛出 ConcurrentModificationException 异常。

import java.util.ArrayList;
import java.util.ListIterator;

public class ListTraversalAndModification {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        // 使用 ListIterator 遍历并修改元素
        ListIterator<Integer> iterator = list.listIterator();
        while (iterator.hasNext()) {
            Integer num = iterator.next();
            if (num.equals(2)) {
                // 使用 ListIterator 的 set 方法修改(替换)元素
                iterator.set(4);
                System.out.println(list); // 输出:[1, 4, 3]
            }
        }
    }
}

对于线程安全的 List,如 CopyOnWriteArrayList,由于其采用了写时复制的机制,在遍历的同时可以进行修改操作,不会抛出 ConcurrentModificationException 异常,但可能会读取到旧的数据,因为修改操作是在新的副本上进行的。

3. List 如何快速删除某个指定下标的元素

(1)ArrayList 提供了 remove(int index) 方法来删除指定下标的元素,该方法在删除元素后,会将后续元素向前移动,以填补被删除元素的位置。如果删除的是列表末尾的元素,时间复杂度为 O(1);如果删除的是列表中间的元素,时间复杂度为 O(n),n 为列表中元素的个数,因为需要移动后续的元素。

import java.util.ArrayList;
import java.util.List;

public class ArrayListRemoveExample {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        // 删除下标为 1 的元素
        list.remove(1);
        System.out.println(list);
    }
}

(2)LinkedList 的 remove(int index) 方法也可以用来删除指定下标的元素。它需要先遍历到指定下标位置,然后修改链表的指针来删除元素。时间复杂度为 O(n),n 为要删除元素的下标。不过,如果已知要删除的元素是链表的头节点或尾节点,可以直接通过修改头指针或尾指针来实现删除,时间复杂度为 O(1)。

import java.util.LinkedList;
import java.util.List;

public class LinkedListRemoveExample {
    public static void main(String[] args) {
        List<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        // 删除下标为 1 的元素
        list.remove(1);
        System.out.println(list);
    }
}

(3)CopyOnWriteArrayList 的 remove 方法同样可以删除指定下标的元素。由于 CopyOnWriteArrayList 在写操作时会创建一个新的数组,所以删除操作的时间复杂度取决于数组的复制速度,通常为 O(n),n 为数组的长度。但在并发环境下,它的删除操作不会影响读操作,具有较好的并发性能。

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListRemoveExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        // 删除下标为 1 的元素
        list.remove(1);
        System.out.println(list);
    }
}

4. ArrayList 和 LinkedList 的区别,哪个集合是线程安全的?

ArrayList 和 LinkedList 都是 Java 中常见的集合类,它们都实现了 List 接口。

底层数据结构不同: ArrayList 使用数组实现,通过索引进行快速访问元素。LinkedList 使用链表实现,通过节点之间的指针进行元素的访问和操作。

插入和删除操作的效率不同: ArrayList 在尾部的插入和删除操作效率较高,但在中间或开头的插入和删除操作效率较低,需要移动元素。LinkedList 在任意位置的插入和删除操作效率都比较高,因为只需要调整节点之间的指针,但是 LinkedList 是不支持随机访问的,所以除了头结点外插入和删除的时间复杂度都是 O(n),效率也不是很高所以 LinkedList 基本没人用。

随机访问的效率不同: ArrayList 支持通过索引进行快速随机访问,时间复杂度为 O(1),LinkedList 需要从头或尾开始遍历链表,时间复杂度为 O(n)。

空间占用: ArrayList 在创建时需要分配一段连续的内存空间,因此会占用较大的空间。LinkedList 每个节点只需要存储元素和指针,因此相对较小。

使用场景: ArrayList 适用于频繁随机访问和尾部的插入删除操作,而 LinkedList 适用于频繁的中间插入删除操作和不需要随机访问的场景。

线程安全: 这两个集合都不是线程安全的,Vector 是线程安全的。

5. ArrayList 和 Vector 的区别是什么?

ArrayList 和 Vector 都是 Java 中常用的动态数组实现,用于存储和操作对象集合,但它们在设计上有几个关键区别,主要体现在线程安全性、性能和功能细节上。

线程安全性: 这是最核心的区别。Vector 是线程安全的,它的大部分方法(比如 add、remove、get 等)都被 synchronized 修饰,这意味着多线程环境下操作 Vector 时,不需要额外处理同步问题。而 ArrayList 没有任何同步机制,是非线程安全的,在多线程并发修改时可能会出现数据不一致的问题,比如抛出 ConcurrentModificationException 异常。

性能: 正因为同步机制的存在,两者在性能上也有差异。由于 Vector 的方法需要加锁释放锁,在单线程环境下,它的操作效率通常比 ArrayList 低。所以如果是单线程场景,或者能自己保证线程安全的情况下,ArrayList 是更优的选择,性能更好。

扩容机制: 当集合元素数量超过初始容量时,都会自动扩容。Vector 默认的扩容策略是翻倍(如果没有指定增长因子的话),比如初始容量 10,满了之后会扩容到 20。而 ArrayList 在 JDK1.8 及之后,默认是扩容为原来的 1.5 倍,相对来说扩容的幅度更小,能在一定程度上节省内存空间。Vector 也可以通过构造方法指定增长因子,灵活控制扩容幅度,而 ArrayList 没有这个功能。

总的来说,选择两者时主要看是否需要线程安全:如果是多线程环境且需要内置同步支持,可能会用到 Vector;但现在更多时候会用 ArrayList,因为它性能更好,而且在需要线程安全时,可以通过 Collections.synchronizedList() 方法将 ArrayList 包装成线程安全的集合,灵活性更高。

6. ArrayList 线程安全吗?把 ArrayList 变成线程安全有哪些方法?

不是线程安全的,ArrayList 变成线程安全的方式有:

使用 Collections 类的 synchronizedList 方法将 ArrayList 包装成线程安全的 List

List<String> synchronizedList = Collections.synchronizedList(arrayList);

使用 CopyOnWriteArrayList 类代替 ArrayList,它是一个线程安全的 List 实现

CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList);

使用 Vector 类代替 ArrayList,Vector 是线程安全的 List 实现

Vector<String> vector = new Vector<>(arrayList);

7. 为什么 ArrayList 线程不安全,哪里不安全?

在高并发添加数据下,ArrayList 会暴露三个问题:

部分值为 null:(我们并没有 add null 进去)当线程 1 走到了扩容那里发现当前 size 是 9,而数组容量是 10,所以不用扩容,这时候 cpu 让出执行权,线程 2 也进来了,发现 size 是 9,而数组容量是 10,所以不用扩容,这时候线程 1 继续执行,将数组下标索引为 9 的位置 set 值了,还没有来得及执行 size++,这时候线程 2 也来执行了,又把数组下标索引为 9 的位置 set 了一遍,这时候两个先后进行 size++,导致下标索引为 10 的地方就为 null 了。

索引越界异常: 线程 1 走到扩容那里发现当前 size 是 9,数组容量是 10 不用扩容,cpu 让出执行权,线程 2 也发现不用扩容,这时候数组的容量就是 10,而线程 1 set 完之后 size++,这时候线程 2 再 set 进来 size 就是 10,数组的大小只有 10,而你要设置下标索引为 10 的就会越界(数组的下标索引从 0 开始);

size 与我们 add 的数量不符: 这个基本上每次都会发生,因为 size++ 本身就不是原子操作,可以分为三步:获取 size 的值,将 size 的值加 1,将新的 size 值覆盖掉原来的,线程 1 和线程 2 拿到一样的 size 值加完了同时覆盖,就会导致一次没有加上,所以肯定不会与我们 add 的数量保持一致的。

为了知道这三种情况是怎么发生的,ArrayList,add 增加元素的代码如下:

public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal() 这个方法的详细代码可以暂时不看,它的作用就是判断如果将当前的新元素加到列表后面,列表的 elementData 数组的大小是否满足,如果 size+1 的这个需求长度大于了 elementData 这个数组的长度,那么就要对这个数组进行扩容。

大体可以分为三步:

(1)判断数组需不需要扩容,如果需要的话,调用 grow 方法进行扩容;

(2)将数组的 size 位置设置值(因为数组的下标是从 0 开始的);

(3)将当前集合的大小加 1

8. ArrayList 和 LinkedList 的应用场景?

ArrayList 适用于需要频繁访问集合元素的场景。它基于数组实现,可以通过索引快速访问元素,因此在按索引查找、遍历和随机访问元素的操作上具有较高的性能。当需要频繁访问和遍历集合元素,并且集合大小不经常改变时,推荐使用 ArrayList。

LinkedList 适用于频繁进行插入和删除操作的场景。它基于链表实现,插入和删除元素的操作只需要调整节点的指针,因此在插入和删除操作上具有较高的性能。当需要频繁进行插入和删除操作,或者集合大小经常改变时,可以考虑使用 LinkedList。

9. ArrayList 的扩容机制说一下

ArrayList 在添加元素时,如果当前元素个数已经达到了内部数组的容量上限,就会触发扩容操作。

ArrayList 的扩容操作主要包括以下几个步骤:

(1)计算新的容量:一般情况下,新的容量会扩大为原容量的 1.5 倍(在 JDK10 之后,扩容策略做了调整),然后检查是否超过了最大容量限制。

(2)创建新的数组:根据计算得到的新容量,创建一个新的更大的数组。

(3)将元素复制:将原来数组中的元素逐个复制到新数组中。

(4)更新引用:将 ArrayList 内部指向原数组的引用指向新数组。

(5)完成扩容:扩容完成后,可以继续添加新元素。

ArrayList 的扩容操作涉及到数组的复制和内存的重新分配,所以在频繁添加大量元素时,扩容操作可能会影响性能。为了减少扩容带来的性能损耗,可以在初始化 ArrayList 时预分配足够大的容量,避免频繁触发扩容操作。之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。

// 新容量计算
int newCapacity = oldCapacity + (oldCapacity >> 1);

10. 线程安全的 List,CopyonWriteArraylist 是如何实现线程安全的

CopyOnWriteArrayList 底层也是通过一个数组保存数据,使用 volatile 关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到。

private transient volatile Object[] array;

在写入操作时,加了一把互斥锁 ReentrantLock 以保证线程安全。

public boolean add(E e) {
    // 获取锁
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取到当前 List 集合保存数据的数组
        Object[] elements = getArray();
        // 获取该数组的长度(这是一个伏笔,同时 len 也是新数组的最后一个元素的索引值)
        int len = elements.length;
        // 将当前数组拷贝一份的同时,让其长度加 1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 将加入的元素放在新数组最后一位,len 不是旧数组长度吗,为什么现在用它当成新数组的最后一个元素的
        newElements[len] = e;
        // 替换引用,将数组的引用指向给新数组的地址
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

看到源码可以知道写入新元素时,首先会先将原来的数组拷贝一份并且让原来数组的长度 +1 后就得到了一个新数组,新数组里的元素和旧数组的元素一样并且长度比旧数组多一个长度,然后将新加入的元素放置都在新数组最后一个位置后,用新数组的地址替换掉老数组的地址就能得到最新的数据了。

在我们执行替换地址操作之前,读取的是老数组的数据,数据是有效数据;执行替换地址操作之后,读取的是新数组的数据,同样也是有效数据,而且使用该方式能比读写都加锁要更加的效率。

现在我们来看读操作,读是没有加锁的,所以读是一直都能读

public E get(int index) {
    return get(getArray(), index);
}

11. List<>里面填基本数据类型为什么会报错?

List<>等泛型集合类要求填充的必须是引用类型(对象类型),而不能直接使用基本数据类型(如 int、char、double 等),否则会编译报错。这是因为 Java 的泛型机制在设计时就只支持引用类型,不支持基本数据类型。

// 错误示例:List 中直接使用基本数据类型 int
// List<int> list = new ArrayList<>(); // 编译报错

// 解决的办法是,使用基本数据类型对应的包装类。
// 正确示例:使用包装类 Integer
List<Integer> list = new ArrayList<>();
list.add(10); // 自动装箱: int->Integer
int num = list.get(0); // 自动拆箱: Integer -> int

这么设计的原因是:

泛型的类型擦除机制: Java 泛型在编译后会被擦除为 object 类型,而 object 只能接收引用类型,不能接收基本数据类型。

历史原因: Java 最初设计时基本数据类型和引用类型是严格区分的,泛型是后期(JDK1.5)才引入的特性,为了兼容已有的类型系统,选择只支持引用类型。

通过使用包装类,结合 Java 的自动装箱(基本类型→包装类)和自动拆箱(包装类→基本类型)机制,可以很方便地在泛型集合中操作基本数据类型的数据。

12. List 和数组如何互转

(1)List 转数组

主要有两种方式,核心是用 List 的 toArray() 方法,重点注意泛型和类型匹配:

① 无参 toArray()(返回 Object[],不推荐)

List<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");
// 返回 object[],强转可能报错
Object[] objArr = strList.toArray();

这种方式返回的是 Object 数组,若强转成 String[] 会抛 ClassCastException,仅适合不确定数组类型的场景,基本不用。

② 带参 toArray(T[] a)(推荐,指定类型)

List<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");

// 方式 1:传入指定长度的数组
String[] strArr1 = strList.toArray(new String[strList.size()]);
// 方式 2:传入空数组(JDK1.8+ 更高效)
String[] strArr2 = strList.toArray(new String[0]);

// 自定义对象 List 转数组
List<User> userList = new ArrayList<>();
userList.add(new User("张三", 20));
User[] userArr = userList.toArray(new User[0]);

这是最常用的方式,传入对应类型的数组,List 会把元素复制到该数组中,若传入的数组长度不足,会自动创建新数组,推荐传空数组(JDK 会优化长度)。

(2)数组转 List

核心是用 Arrays.asList(),但要注意返回的 List 不可变和基本类型数组的坑:

① 普通对象数组转 List(常用)

String[] strArr = {"a", "b", "c"};
// 返回固定大小的 List(属于 Arrays 内部类,不可 add/remove)
List<String> strList1 = Arrays.asList(strArr);

// 若需要可变 List,包装一层 ArrayList
List<String> strList2 = new ArrayList<>(Arrays.asList(strArr));
strList2.add("d"); // 正常执行

Arrays.asList() 返回的 List 不是 ArrayList,而是 Arrays 的内部类,不支持添加/删除操作,想修改就套一层 ArrayList。

② 基本类型的数组转 List(避坑)

// 错误示例:int[]转 List 会变成 List<int[]>,而非 List<Integer>
int[] numArr = {1, 2, 3};
List<int[]> wrongList = Arrays.asList(numArr);

// 正确方式 1:手动装箱(JDK8-)
List<Integer> numList1 = new ArrayList<>();
for (int num : numArr) {
    numList1.add(num);
}

// 正确方式 2:Stream 流(JDK8+)
List<Integer> numList2 = Arrays.stream(numArr).boxed().collect(Collectors.toList());

基本类型数组 (int[]、long[]) 直接用 Arrays.asList() 会把整个数组当成一个元素,必须手动装箱或用 Stream 流转换为包装类 (Integer) 的 List。

目录

  1. 1. List 的几种实现
  2. 2. List 可以一边遍历一边修改元素吗
  3. 3. List 如何快速删除某个指定下标的元素
  4. 4. ArrayList 和 LinkedList 的区别,哪个集合是线程安全的?
  5. 5. ArrayList 和 Vector 的区别是什么?
  6. 6. ArrayList 线程安全吗?把 ArrayList 变成线程安全有哪些方法?
  7. 7. 为什么 ArrayList 线程不安全,哪里不安全?
  8. 8. ArrayList 和 LinkedList 的应用场景?
  9. 9. ArrayList 的扩容机制说一下
  10. 10. 线程安全的 List,CopyonWriteArraylist 是如何实现线程安全的
  11. 11. List<>里面填基本数据类型为什么会报错?
  12. 12. List 和数组如何互转

更多推荐文章

查看全部
  • Arduino BLDC 驱动方案:MimiClaw + ESP32 嵌入式机器人实战
  • Python 临床知识问答与检索项目架构设计与实现
  • Linux 进程信号入门:软中断机制与实战
  • LLama-Factory 集成 HuggingFace 镜像加速模型下载与训练
  • 群消息机器人统计方案选型与实现
  • 人工智能、机器学习与深度学习的本质区别
  • Python 标准库与第三方库实战:日期处理与 Excel 操作
  • Python 实现 AI 绘画用户评价自动分析与报告生成
  • Dify 工作流发布为 MCP Server 实战指南
  • Quartus Prime 新手入门实战指南
  • Spring AI 实战:从零开发 IDEA 插件版 AI 代码助手(Java 全栈 + 上下文感知)
  • 前端开发实战:一天到底能做多少个页面?
  • Python 标准库与第三方库实战:日期处理与 Excel 操作
  • Java 注解与反射实战:自定义日志与参数校验注解实现
  • AI 学习路线图:从基础到 PyTorch 深度学习实战
  • OpenClaw 构建专业 AI 角色配置指南
  • RoboChallenge 具身智能年度报告:4 万次真机评测揭示模型真实水平
  • 基于 KSWEB 与 cpolar 在安卓手机部署 Typecho 博客
  • Java Desktop 自动化测试插件:支持 VS Code 与 Cursor 的 AI 方案
  • Phi-3-mini-128k-instruct Chainlit 插件开发:思维链可视化与 Token 统计

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online