在 Java 中,List
是一个非常常用的接口,它代表有序、可重复的集合。常见的实现类有:
ArrayList(基于动态数组,查询快、增删慢)
LinkedList(基于双向链表,增删快、查询慢)
Vector(早期的线程安全动态数组,现在基本被 ArrayList 替代)
本篇文章将从 代码示例 + 文字解释 + 原理 + 优缺点 四个方面,带你全面掌握它们的区别与使用场景。
一、ArrayList
关键词: 集合类、动态数组、泛型、安全存储
在 Java 中,ArrayList
是最常用的集合类之一,它属于 java.util
包,是一个可变长数组——和数组不同的是,它的长度可以动态增加或减少,非常适合在数据量不固定的场景下使用。
下面我们通过一个“城市列表”的案例,逐行讲解常用语法。
1. 创建 ArrayList 并添加元素
import java.util.ArrayList; // 导入 ArrayList 类
public class ArrayListDemo {
public static void main(String[] args) {
// 1. 创建 ArrayList 对象(存储 String 类型数据)
ArrayList<String> cities = new ArrayList<>();
// 2. 添加城市名称
cities.add("北京");
cities.add("上海");
cities.add("广州");
// 3. 打印城市列表
System.out.println("城市列表:" + cities);
// 4. 获取列表大小
System.out.println("城市数量:" + cities.size());
// 5. 根据索引获取元素
System.out.println("索引 1 的城市是:" + cities.get(1));
}
}
解释:
import java.util.ArrayList;
作用:导入 Java 提供的ArrayList
类。
背景:集合类位于java.util
包,不导入会编译报错。ArrayList<String> cities = new ArrayList<>();
作用:创建一个只能存储 String 的ArrayList
。
<String>
是泛型,让集合类型更安全。cities.add("北京");
作用:向末尾添加一个元素。
注意:ArrayList
索引从 0 开始。System.out.println("城市列表:" + cities);
作用:直接打印列表,会以[元素1, 元素2, ...]
形式输出。cities.size()
作用:获取当前元素个数(方法而非属性)。cities.get(1)
作用:按索引取元素,这里是第二个城市“上海”。
注意:越界会抛IndexOutOfBoundsException
。
2. 在指定位置插入和替换元素
cities.add(1, "深圳");
System.out.println("插入后的列表:" + cities);
cities.set(2, "杭州");
System.out.println("替换后的列表:" + cities);
解释:
add(int index, E element)
:在指定位置插入新元素(原元素后移)。set(int index, E element)
:替换指定位置的元素。区别:
add()
插入,set()
覆盖。
3. 删除元素
cities.remove(3); // 按索引删除
System.out.println("删除索引3的元素后:" + cities);
cities.remove("深圳"); // 按内容删除
System.out.println("删除深圳后:" + cities);
解释:
remove(index)
根据索引删除元素,后面的元素会自动前移。remove(Object)
根据元素值删除,匹配到第一个符合的元素就移除。
4. 判断是否存在
System.out.println("是否包含北京:" + cities.contains("北京"));
System.out.println("是否包含天津:" + cities.contains("天津"));
解释:
contains(Object)
检查列表中是否包含某个元素,返回true
或false
。
5. 遍历 ArrayList
System.out.println("=== for 循环遍历 ===");
for (int i = 0; i < cities.size(); i++) {
System.out.println("索引 " + i + " : " + cities.get(i));
}
System.out.println("=== 增强 for 遍历 ===");
for (String city : cities) {
System.out.println(city);
}
System.out.println("=== forEach 方法遍历 ===");
cities.forEach(System.out::println);
解释:
普通 for 循环:可通过索引灵活访问元素。
增强 for 循环:更简洁,直接获取元素值。
forEach 方法:使用 Lambda 表达式或方法引用遍历。
6. 清空列表
cities.clear();
System.out.println("清空后的城市列表:" + cities);
解释:
clear()
移除所有元素,使列表变为空,但对象依然存在,可继续添加元素。
小结
底层结构:动态数组(Object[]),支持自动扩容。
访问速度:按索引访问快(O(1))。
插入/删除:中间插入或删除慢(O(n)),尾部操作快。
线程安全:非线程安全,需要手动同步。
适用场景:读操作多、随机访问频繁,元素数量变化不大时最合适。
常用方法:
add()
、add(index, element)
、set()
、remove()
、get()
、size()
、contains()
、clear()
。
二、LinkedList
关键词:集合类、双向链表、插入删除高效、支持队列/栈
在 Java 中,LinkedList
是基于 双向链表 实现的 List,它属于 java.util
包。
与 ArrayList
不同,它的优势是在 任意位置插入和删除元素 时效率更高。
下面通过一个“任务队列”的案例,逐行讲解常用语法。
1. 创建 LinkedList 并添加元素
import java.util.LinkedList; // 导入 LinkedList 类
public class LinkedListDemo1 {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>(); // 创建空链表
tasks.add("写周报"); // 末尾添加
tasks.add("整理文件");
tasks.add("开会");
System.out.println("初始化:" + tasks); // [写周报, 整理文件, 开会]
tasks.addFirst("检查邮件"); // 头部添加
tasks.addLast("提交总结"); // 尾部添加
tasks.add(2, "打印资料"); // 指定索引插入
System.out.println("添加后的任务列表:" + tasks);
System.out.println("任务数量:" + tasks.size());
}
}
解释:
LinkedList<String> tasks = new LinkedList<>();
创建只能存储String
的链表,底层节点带前驱/后继指针。add(E e)
(末尾添加)
将元素链接到尾节点之后。⏱ 一般为 O(1)。addFirst(E e)
/addLast(E e)
在头/尾插入;双向链表此操作非常快。⏱ O(1)。add(int index, E e)
在指定位置插入,需要遍历到该位置再改指针。⏱ O(n)。size()
返回元素个数。实现中会维护计数,因此是 ⏱ O(1)。
小提示:
LinkedList
打印时会调用toString()
,格式如[元素A, 元素B, ...]
。
2. 获取元素(按索引 / 首尾 / 非抛异常版)
import java.util.LinkedList;
public class LinkedListDemo2 {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
tasks.add("写周报");
tasks.add("整理文件");
tasks.add("开会");
System.out.println("第一个任务:" + tasks.getFirst()); // 抛异常版
System.out.println("最后一个任务:" + tasks.getLast());
System.out.println("索引1的任务:" + tasks.get(1)); // 随机访问
// 不抛异常的安全获取(空表返回 null)
System.out.println("peekFirst:" + tasks.peekFirst());
System.out.println("peekLast :" + tasks.peekLast());
System.out.println("peek :" + tasks.peek()); // 等价于 peekFirst
}
}
解释:
getFirst()/getLast()
获取首/尾元素;空链表时抛NoSuchElementException
。⏱ O(1)。get(int index)
需要从头或尾走到该位置,链表随机访问慢。⏱ O(n)。peekFirst()/peekLast()/peek()
不抛异常的获取版本:空表返回null
,用于更稳健的代码。
易错点:把
getFirst()
当成“安全获取”会在空链表下崩溃,空表请用peekFirst()
。
3. 在指定位置插入与替换元素
import java.util.LinkedList;
public class LinkedListDemo3 {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
tasks.add("写周报");
tasks.add("整理文件");
tasks.add("开会");
tasks.addFirst("检查邮件"); // 首部插入
tasks.add(2, "领取物料"); // 指定索引插入
String old = tasks.set(1, "打印资料"); // 替换并返回被替换的旧值
System.out.println("被替换的任务:" + old);
System.out.println("更新后的任务列表:" + tasks);
}
}
代码讲解:
addFirst(E e)
/add(int index, E e)
插入元素;add(index, e)
需要先定位再改指针。set(int index, E e)
覆盖指定位置元素并返回旧值。区别:
add(index, e)
是插入(元素后移),set(index, e)
是覆盖。
边界:索引越界会抛
IndexOutOfBoundsException
(index < 0 || index > size
【插入】;index >= size
【set/get】)。
4. 查询与定位元素
import java.util.LinkedList;
public class LinkedListDemo4 {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
tasks.add("开会");
tasks.add("写周报");
tasks.add("开会"); // 重复元素
tasks.add("整理文件");
System.out.println("是否包含'开会':" + tasks.contains("开会"));
System.out.println("第一次出现'开会'的位置:" + tasks.indexOf("开会"));
System.out.println("最后一次出现'开会'的位置:" + tasks.lastIndexOf("开会"));
}
}
代码讲解:
contains(Object o)
线性查找是否存在。⏱ O(n)。indexOf(Object o)
/lastIndexOf(Object o)
从前/从后查找第一个匹配位置。⏱ O(n)。
5. 删除元素(首/尾/索引/值 & 非抛异常版)
import java.util.LinkedList;
public class LinkedListDemo5 {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
tasks.add("检查邮件");
tasks.add("写周报");
tasks.add("整理文件");
tasks.add("开会");
String first = tasks.removeFirst(); // 删除首元素(抛异常版)
String last = tasks.removeLast(); // 删除尾元素
String byIdx = tasks.remove(1); // 删除索引1的元素
boolean byVal = tasks.remove("不存在的任务"); // 按值删除,未删返回 false
System.out.println("removeFirst:" + first);
System.out.println("removeLast :" + last);
System.out.println("remove(1) :" + byIdx);
System.out.println("remove(o) :" + byVal);
System.out.println("删除后的任务列表:" + tasks);
// 不抛异常的删除(空表返回 null)
tasks.clear();
System.out.println("pollFirst:" + tasks.pollFirst()); // null
System.out.println("pollLast :" + tasks.pollLast()); // null
System.out.println("poll :" + tasks.poll()); // 等价于 pollFirst
}
}
代码讲解:
removeFirst()/removeLast()/remove(int)
删除并返回元素;空表/越界会抛异常。remove(Object o)
删除第一个匹配的元素,返回是否成功。pollFirst()/pollLast()/poll()
不抛异常的删除:空表返回null
,推荐在未知是否为空时使用。
6. 遍历方式(for / foreach / Iterator / 逆序)
import java.util.Iterator;
import java.util.LinkedList;
public class LinkedListDemo6 {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
tasks.add("写周报");
tasks.add("整理文件");
tasks.add("开会");
// 1) for 索引遍历(不建议:链表随机访问慢)
for (int i = 0; i < tasks.size(); i++) {
System.out.println("索引" + i + ":" + tasks.get(i));
}
// 2) 增强 for(推荐)
for (String t : tasks) {
System.out.println("foreach:" + t);
}
// 3) Iterator(可边遍历边安全删除)
for (Iterator<String> it = tasks.iterator(); it.hasNext(); ) {
String t = it.next();
if (t.contains("周报")) it.remove(); // 迭代器删除,避免并发修改异常
}
System.out.println("迭代器删除后:" + tasks);
// 4) 逆序遍历(尾 -> 头)
Iterator<String> dit = tasks.descendingIterator();
while (dit.hasNext()) {
System.out.println("逆序:" + dit.next());
}
}
}
代码讲解:
链表不适合
for (i)
访问:get(i)
是 O(n)。Iterator
的remove()
:唯一安全的遍历时删除方式;否则易抛ConcurrentModificationException
。descendingIterator()
:从尾到头的逆序迭代。
7. 作为队列 / 栈使用(Deque 能力)
import java.util.LinkedList;
public class LinkedListDemo7 {
public static void main(String[] args) {
// 队列:FIFO
LinkedList<String> queue = new LinkedList<>();
queue.offer("任务A"); // == addLast
queue.offer("任务B");
System.out.println("队首查看:" + queue.peek()); // == peekFirst
System.out.println("出队:" + queue.poll()); // == pollFirst
System.out.println("队列状态:" + queue);
// 栈:LIFO
LinkedList<String> stack = new LinkedList<>();
stack.push("命令1"); // == addFirst
stack.push("命令2");
System.out.println("栈顶查看:" + stack.peek()); // == peekFirst
System.out.println("出栈:" + stack.pop()); // == removeFirst
System.out.println("栈状态:" + stack);
}
}
代码讲解:
队列 API:
offer
(入队)、peek
(窥视队首)、poll
(出队)。栈 API:
push
(压栈)、peek
(窥视栈顶)、pop
(出栈)。LinkedList
实现了Deque
接口,所以可同时充当队列/双端队列/栈。
8. 其他常用操作与注意点
import java.util.Arrays;
import java.util.LinkedList;
public class LinkedListDemo8 {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>(Arrays.asList("写周报", "整理文件", "开会"));
System.out.println("是否为空:" + tasks.isEmpty()); // false
tasks.removeIf(t -> t.contains("报")); // 按条件删除
System.out.println("按条件删除后:" + tasks);
// 转为数组(保留顺序)
String[] arr = tasks.toArray(new String[0]);
System.out.println("数组长度:" + arr.length);
}
}
代码讲解:
isEmpty()
:是否无元素。removeIf(Predicate)
:按条件批量删除,Java 8+。toArray(new String[0])
:转数组(推荐写法,类型安全)。
提醒:
LinkedList
更适合增删多的场景;若主要是队列/栈功能,ArrayDeque
往往更快、更省内存。
小结
优点:插入/删除效率高(尤其首尾 O(1));支持丰富的 Deque(队列/栈)操作。
缺点:随机访问慢(
get(index)
为 O(n)),比数组结构更占内存。使用建议:
增删多 →
LinkedList
(或优先ArrayDeque
做队列/栈)。查找多 →
ArrayList
。遍历删除请用
Iterator.remove()
;空表访问用peek/poll
的非抛异常方法。
三、Vector(不推荐新项目使用)
关键词:线程安全、同步、性能低于 ArrayList
Vector
是一个 线程安全 的动态数组类,和 ArrayList
功能类似,但所有对数据的操作方法(如 add
、remove
、set
等)都被 synchronized
修饰,保证了在多线程环境下的安全性。
缺点是:锁的开销较大,在单线程下性能明显低于 ArrayList
。
目前的推荐做法是:用 ArrayList
+ 自己的同步机制(如 Collections.synchronizedList()
或 CopyOnWriteArrayList
)替代。
1. 创建 Vector 并添加元素
import java.util.Vector;
public class VectorDemo {
public static void main(String[] args) {
// 创建一个存储字符串的线程安全动态数组
Vector<String> books = new Vector<>();
// 添加元素
books.add("Java 核心技术");
books.add("算法导论");
books.add("设计模式");
// 输出集合内容
System.out.println("书籍列表:" + books);
System.out.println("数量:" + books.size());
}
}
代码讲解:
Vector<String> books = new Vector<>();
创建一个线程安全的Vector
,默认容量为 10,超过容量会自动扩容(扩容规则是容量 * 2)。add()
在集合末尾添加元素,内部会加锁确保多线程下数据不冲突。size()
返回当前元素的数量。
2. 插入、替换、删除元素
books.add(1, "计算机网络"); // 插入到索引 1
books.set(2, "操作系统"); // 替换索引 2 位置的元素
books.remove("Java 核心技术"); // 按值删除
books.remove(0); // 按索引删除
System.out.println("更新后的书籍列表:" + books);
代码讲解:
add(index, element)
在指定位置插入元素,原位置及后面的元素依次后移。set(index, element)
替换指定位置的元素。remove(Object)
删除与参数值相等的第一个元素。remove(index)
删除指定索引位置的元素。
这些方法与
ArrayList
一致,区别在于Vector
内部所有方法都加了synchronized
锁,可以防止多线程同时修改导致数据错乱。
3. 遍历 Vector
// for-each 遍历
for (String book : books) {
System.out.println(book);
}
// 传统 for 遍历
for (int i = 0; i < books.size(); i++) {
System.out.println(books.get(i));
}
// 使用 Enumeration(Vector 特有)
import java.util.Enumeration;
Enumeration<String> enumeration = books.elements();
while (enumeration.hasMoreElements()) {
System.out.println(enumeration.nextElement());
}
代码讲解:
for-each:最常用的遍历方式,简洁明了。
传统 for:可通过索引访问,方便进行条件判断。
Enumeration:
Vector
独有的遍历方式,早期 Java 版本遗留的接口,现在更多使用Iterator
。
4. Vector 的线程安全原理
Vector
的线程安全是通过 方法级的 synchronized
修饰实现的,例如 add()
方法源码:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
synchronized
:方法锁,保证同一时刻只有一个线程能调用该方法。缺点:锁粒度太大,单线程环境下会导致性能下降。
5. 扩容机制
初始容量为 10。
每次扩容为 原容量的 2 倍。
如果指定了
initialCapacity
构造参数,初始容量会按你的参数设置。
示例:
Vector<Integer> nums = new Vector<>(5); // 初始容量 5
for (int i = 0; i < 10; i++) {
nums.add(i);
System.out.println("容量:" + nums.capacity());
}
小结
优点:线程安全,适合多线程共享数据。
缺点:单线程下性能差,扩容成本高。
适用场景:需要线程安全的少量数据存储(现在很少用,更多用
ArrayList
+ 自定义同步机制)。
四、List 集合总结对比表
特性 | ArrayList | LinkedList | Vector |
---|---|---|---|
底层结构 | 动态数组(Object[]) | 双向链表 | 动态数组(Object[]) |
访问速度 | 快(O(1) 按索引访问) | 慢(O(n) 按索引访问) | 快(O(1) 按索引访问) |
插入/删除 | 慢(中间插入需要移动元素) | 快(只需修改节点指针) | 慢(同 ArrayList) |
线程安全 | 否(需手动加锁) | 否(需手动加锁) | 是(所有方法都有 synchronized) |
内存占用 | 较低(仅存储数据) | 较高(需额外存储前驱/后继指针) | 较低(仅存储数据) |
适用场景 | 大量读、少量改;随机访问频繁 | 大量插入/删除,尤其是中间位置 | 多线程且需要线程安全(现在较少使用) |
总结建议
单线程、大量查询 → 用 ArrayList(性能最好)。
频繁插入/删除(中间位置) → 用 LinkedList。
多线程必须线程安全 → 用 Vector(或
Collections.synchronizedList(new ArrayList<>())
)。如果想兼顾性能和安全,可以用
CopyOnWriteArrayList
(并发包提供,读多写少时很高效)。