List 与 ArrayList
本教程共 100 篇 · 第 73 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
本节目标:搞懂 List 的「有序可重复」特性,认清 ArrayList 是动态数组,学完能熟练用它的增删查改方法并正确遍历。
List 是什么
List 是 Collection 家族里最常用的一支。它有三个特征,记住就行:
- 有序:先加的排在前面,后加的在后面,顺序不会乱。
- 可重复:同一个元素能出现多次,允许装两个「苹果」。
- 可索引:像数组一样用下标 0、1、2 去取第几个。
List<String> fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("苹果"); // 重复也没问题
System.out.println(fruits.get(0)); // 苹果
正因为可重复、可索引,List 用起来最像我们直觉里的「一列数据」,所以它是初学者最先该掌握的集合。
ArrayList 的本事
ArrayList 是 List 的默认实现,也是你日常最该先用的那个。它底层其实是一个数组,但比普通数组聪明:装满了会自动把容量翻倍,腾出地方继续装。
// 初始容量 10,不够时自动扩容,你完全不用操心长度
List<Integer> nums = new ArrayList<>();
for (int i = 0; i < 100; i++) {
nums.add(i); // 随便加,永远不报「下标越界」
}
Tip如果你大概知道要装多少,可以在构造时给个初始容量,减少扩容次数:
new ArrayList<>(1000)。数据量小的时候不用纠结这个,默认行为足够好。
最常用的几个方法
下面这些方法是 List 的日常操作,建议敲一遍记牢。
List<String> list = new ArrayList<>();
list.add("A"); // 末尾追加
list.add(0, "插队"); // 在下标 0 插入
list.set(1, "改掉"); // 替换下标 1 的元素
String s = list.get(1); // 取下标 1
list.remove(0); // 按下标删
list.remove("A"); // 按内容删
int n = list.size(); // 元素个数
int idx = list.indexOf("A"); // 找不到返回 -1
boolean has = list.contains("A"); // 是否包含
boolean empty = list.isEmpty(); // 是否为空
list.clear(); // 清空所有元素
注意 add 和 set 的区别:add 是塞进去(后面元素后移、个数+1),set 是替换(个数不变)。indexOf 查不到返回 -1,别误以为是返回 0 或抛异常。
Warning
remove(int)按下标删,remove(Object)按内容删。当你传的是数字字面量时容易踩坑:list.remove(1)删的是下标 1,不是内容1。要删数字内容得写成list.remove(Integer.valueOf(1))。
三种遍历方式
List 有三种常见遍历,按场景选。
// 1. 普通 for,需要下标时用
for (int i = 0; i < list.size(); i++) {
System.out.println(i + ":" + list.get(i));
}
// 2. 增强 for,最省事,只读遍历首选
for (String item : list) {
System.out.println(item);
}
// 3. 迭代器,遍历中删除元素时用(第 81 章细讲)
for (var it = list.iterator(); it.hasNext(); ) {
String item = it.next();
if (item.isEmpty()) it.remove();
}
Note如果只是从头到尾读一遍,用增强 for(
for (String item : list))最清爽,不用管下标。迭代器删除是遍历中改 List 的唯一安全方式,别在 for 循环里直接list.remove(item),会触发并发修改异常。
ArrayList 的强项和软肋
ArrayList 的优势是「查得快」:按下标 get(i) 是一步到位,不论列表多长都很快。它的软肋在「中间增删慢」:在中间插入或删除一个元素,后面所有的元素都要整体挪动位置。
// 末尾追加很快,中间插入较慢
list.add("末尾"); // 快
list.add(0, "开头"); // 慢,后面全员后移
如果要在中间大量增删,每次挪动的成本会累积,这时候就该考虑下一章的 LinkedList。但对绝大多数业务代码来说,数据量不大、增删不频繁,ArrayList 的速度完全够用,不必过早优化。
和数组互转
List 和数组经常要互相转换。
// List 转数组
List<String> list = List.of("a", "b", "c");
String[] arr = list.toArray(new String[0]);
// 数组转 List
String[] arr2 = {"x", "y"};
List<String> list2 = Arrays.asList(arr2); // 注意:长度固定,不能增删
Warning
Arrays.asList(...)返回的 List 背后还是那个数组,长度固定,调用add/remove会抛UnsupportedOperationException。想要能增删的,再包一层new ArrayList<>(Arrays.asList(arr2))。
取一段:subList
有时你想临时操作列表里的「连续一段」,可以用 subList:
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> part = list.subList(1, 4); // 取下标 [1,4),即 [2,3,4]
System.out.println(part); // [2, 3, 4]
Warning
subList返回的是原列表的「视图」,不是副本。你改了视图,原列表也会变;原列表结构一变,subList就失效会抛异常。要独立的一份就new ArrayList<>(list.subList(...))。
容量与扩容的细节
ArrayList 刚创建时容量是 0,第一次 add 才扩到 10(具体版本可能微调),之后满了就扩到原来的 1.5 倍左右。扩容的代价是新建一个更大的数组、把旧数据拷过去,所以一次性 add 很多元素时,提前给初始容量能少扩几次。你可以用 ensureCapacity(int) 提前预约容量,也能用 trimToSize() 在确认不再增长后把多余空间还回去,这两个方法日常很少用,知道有就行。
一个能跑的完整例子
把前面这些方法串起来,写个真实点的小程序:录入若干商品名,演示按下标改、按内容删,最后用下标 for 遍历打印。
import java.util.*;
public class ListDemo {
public static void main(String[] args) {
List<String> goods = new ArrayList<>(List.of("苹果", "香蕉", "苹果"));
goods.set(1, "橙子");
goods.remove("香蕉");
for (int i = 0; i < goods.size(); i++) {
System.out.println(i + "号:" + goods.get(i));
}
}
}
这段程序里,List.of 先给出初始数据,省得一个个 add;set 改了下标 1 的值;remove("香蕉") 按内容删掉了香蕉;最后用带下标的 for 打印出每个元素的位置和内容。你可以照着改几个值,感受顺序和长度的变化。
实际写代码时怎么挑
ArrayList 在日常业务里无处不在:把数据库查出的一批用户装进列表逐个处理,把表单里勾选的多个选项收集起来,把接口返回的数组转成列表再排序,底层大多都是它。它和数组最大的差别就一句:长度随用随长,你永远不用在写代码时猜到底要装多少。
新手常问 ArrayList 线程安全吗?答案是不安全,多线程同时改会乱。你初学写的几乎都是单线程小程序,暂时不用管;等学到多线程,再考虑用同步包装或者并发包里的实现。先把单线程下的增删查改练熟更重要。判断该不该用 List,记住一句:数据既有顺序、又可能重复,就用 List;如果不要重复,去看第 75 章的 Set。
小结
List 是有序、可重复、能按索引访问的集合;ArrayList 是它的默认实现,底层动态数组,查得快、中间增删慢。日常增删查改用 add/get/set/remove/size,遍历优先用增强 for。下一章看 LinkedList,以及该绕开的历史类 Vector 和 Stack。