跳到主要内容 Java List 集合详解 | 极客日志
Java java 算法
Java List 集合详解 详细讲解了 Java List 接口的几种常见实现类,包括 ArrayList、LinkedList、Vector 和 CopyOnWriteArrayList。重点分析了它们在底层数据结构、线程安全性、性能表现及扩容机制上的区别。文章还探讨了在遍历过程中修改元素的正确方式,避免并发修改异常,并提供了 ArrayList 转数组、数组转 List 的常用方法及注意事项。最后解释了泛型不支持基本数据类型的原因及自动装箱拆箱机制。
KernelLab 发布于 2026/3/21 更新于 2026/4/18 4 浏览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 (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 {
{
List<Integer> list = <>();
list.add( );
list.add( );
list.add( );
(Integer num : list) {
(list.indexOf(num) != - ) {
list.set(list.indexOf(num), num * );
}
System.out.println(list);
}
}
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 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
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
ListTraversalAndModification
public
static
void
main
(String[] args)
new
ArrayList
1
2
3
for
if
1
2
(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<Integer> iterator = list.listIterator();
while (iterator.hasNext()) {
Integer num = iterator.next();
if (num.equals(2 )) {
iterator.set(4 );
System.out.println(list);
}
}
}
}
对于线程安全的 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 );
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 );
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 );
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 );
elementData[size++] = e;
return true ;
}
ensureCapacityInternal() 这个方法的详细代码可以暂时不看,它的作用就是判断如果将当前的新元素加到列表后面,列表的 elementData 数组的大小是否满足,如果 size+1 的这个需求长度大于了 elementData 这个数组的长度,那么就要对这个数组进行扩容。
(1)判断数组需不需要扩容,如果需要的话,调用 grow 方法进行扩容;
(2)将数组的 size 位置设置值(因为数组的下标是从 0 开始的);
8. ArrayList 和 LinkedList 的应用场景? ArrayList 适用于需要频繁访问集合元素的场景 。它基于数组实现,可以通过索引快速访问元素,因此在按索引查找、遍历和随机访问元素的操作上具有较高的性能。当需要频繁访问和遍历集合元素,并且集合大小不经常改变时,推荐使用 ArrayList。
LinkedList 适用于频繁进行插入和删除操作的场景 。它基于链表实现,插入和删除元素的操作只需要调整节点的指针,因此在插入和删除操作上具有较高的性能。当需要频繁进行插入和删除操作,或者集合大小经常改变时,可以考虑使用 LinkedList。
9. ArrayList 的扩容机制说一下 ArrayList 在添加元素时,如果当前元素个数已经达到了内部数组的容量上限,就会触发扩容操作。
ArrayList 的扩容操作主要包括以下几个步骤:
(1)计算新的容量:一般情况下,新的容量会扩大为原容量的 1.5 倍(在 JDK10 之后,扩容策略做了调整),然后检查是否超过了最大容量限制。
(2)创建新的数组:根据计算得到的新容量,创建一个新的更大的数组。
(3)将元素复制:将原来数组中的元素逐个复制到新数组中。
(4)更新引用:将 ArrayList 内部指向原数组的引用指向新数组。
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 {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1 );
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<Integer> list = new ArrayList <>();
list.add(10 );
int num = list.get(0 );
泛型的类型擦除机制: Java 泛型在编译后会被擦除为 object 类型,而 object 只能接收引用类型,不能接收基本数据类型。
历史原因: Java 最初设计时基本数据类型和引用类型是严格区分的,泛型是后期(JDK1.5)才引入的特性,为了兼容已有的类型系统,选择只支持引用类型。
通过使用包装类,结合 Java 的自动装箱(基本类型→包装类)和自动拆箱(包装类→基本类型)机制,可以很方便地在泛型集合中操作基本数据类型的数据。
12. List 和数组如何互转 主要有两种方式,核心是用 List 的 toArray() 方法,重点注意泛型和类型匹配:
① 无参 toArray() (返回 Object[],不推荐)
List<String> strList = new ArrayList <>();
strList.add("a" );
strList.add("b" );
Object[] objArr = strList.toArray();
这种方式返回的是 Object 数组,若强转成 String[] 会抛 ClassCastException,仅适合不确定数组类型的场景,基本不用。
② 带参 toArray(T[] a) (推荐,指定类型)
List<String> strList = new ArrayList <>();
strList.add("a" );
strList.add("b" );
String[] strArr1 = strList.toArray(new String [strList.size()]);
String[] strArr2 = strList.toArray(new String [0 ]);
List<User> userList = new ArrayList <>();
userList.add(new User ("张三" , 20 ));
User[] userArr = userList.toArray(new User [0 ]);
这是最常用的方式,传入对应类型的数组,List 会把元素复制到该数组中,若传入的数组长度不足,会自动创建新数组,推荐传空数组(JDK 会优化长度)。
核心是用 Arrays.asList(),但要注意返回的 List 不可变和基本类型数组的坑:
String[] strArr = {"a" , "b" , "c" };
List<String> strList1 = Arrays.asList(strArr);
List<String> strList2 = new ArrayList <>(Arrays.asList(strArr));
strList2.add("d" );
Arrays.asList() 返回的 List 不是 ArrayList,而是 Arrays 的内部类,不支持添加/删除操作,想修改就套一层 ArrayList。
int [] numArr = {1 , 2 , 3 };
List<int []> wrongList = Arrays.asList(numArr);
List<Integer> numList1 = new ArrayList <>();
for (int num : numArr) {
numList1.add(num);
}
List<Integer> numList2 = Arrays.stream(numArr).boxed().collect(Collectors.toList());
基本类型数组 (int[]、long[]) 直接用 Arrays.asList() 会把整个数组当成一个元素,必须手动装箱或用 Stream 流转换为包装类 (Integer) 的 List。