List 接口与有序集合 | JavaSE
List 接口与有序集合
一、学习目标
完成本章后,你应该能够:
- 能够解释
List<E>在 Java 集合框架中的定位,以及“有序、可重复、有索引”的具体含义。 - 能够正确使用
add(index, element)、get(index)、set(index, element)、remove(index)完成按位置增删改查。 - 能够区分
remove(int index)与remove(Object o),特别是正确处理List<Integer>的重载陷阱。 - 能够使用普通索引 for、Iterator、增强 for、
forEach遍历 List,并根据需求选择合适方式。 - 能够理解 List 的索引边界以及常见
IndexOutOfBoundsException。 - 能够掌握 JDK 21 中 List 新增的首尾操作和
reversed()反向视图。 - 能够区分“有序”与“排序”,避免把 List 的顺序语义理解成自动排序。
二、核心知识
2.1 List 是什么
List<E> 是 Java 集合框架中的一个核心接口。
可以把它理解为:
具有明确元素先后位置的一组数据。
例如:
0 → Java
1 → MySQL
2 → Redis
3 → Java
这个数据模型非常适合使用:
List<String>
表示。
List 在 Collection 的基础上增加了一个非常重要的概念:
位置(Position)。
因此 List 不仅可以:
添加
删除
判断包含
获取数量
遍历
还可以:
按照索引获取元素
按照索引插入元素
按照索引修改元素
按照索引删除元素
2.2 List 在集合体系中的位置
在 JDK 21 中,可以把主要关系理解为:
Iterable<E>
│
Collection<E>
│
SequencedCollection<E>
│
List<E>
│
├── ArrayList<E>
└── LinkedList<E>
其中:
Collection<E>
提供集合通用能力。
SequencedCollection<E>
描述具有明确 encounter order 的集合。
List<E>
进一步提供:
基于整数索引的位置访问
能力。
2.3 List 的三个核心特点
JavaSE 学习阶段通常将 List 总结为:
有序、可重复、有索引。
这三个词必须真正理解。
2.4 特点一:有序
这里的“有序”主要指:
元素具有明确的 encounter order。
例如:
List<String> technologies =
new ArrayList<>();
technologies.add("Java");
technologies.add("MySQL");
technologies.add("Redis");
逻辑顺序是:
0 → Java
1 → MySQL
2 → Redis
遍历时也会按照 List 定义的元素顺序访问。
注意:
有序不等于排序。
例如:
List<Integer> numbers =
new ArrayList<>();
numbers.add(100);
numbers.add(5);
numbers.add(70);
List 不会自动变成:
5
70
100
而仍然是:
100
5
70
所以:
有序
= 有明确的先后位置
排序
= 按某种大小或比较规则重新排列
这是两个不同概念。
2.5 特点二:允许重复
List 可以保存逻辑上重复的元素。
例如:
List<String> names =
new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("张三");
结果可以是:
[张三, 李四, 张三]
两个 "张三" 可以同时存在。
因此:
List
适合:
允许重复记录
的业务数据。
例如:
浏览历史
商品搜索记录
考试成绩列表
日志记录
文章列表
订单明细
2.6 特点三:有索引
List 和数组一样:
索引从
0开始。
例如:
List:
索引 元素
0 Java
1 MySQL
2 Redis
因此:
list.get(0)
表示:
第一个元素
而不是第二个。
如果 List 有:
list.size()
个元素,则合法元素索引范围通常是:
0 ~ size() - 1
例如:
size = 4
合法索引:
0
1
2
3
2.7 List 与 Collection 的关系
List:
List<E>
属于 Collection 体系。
因此 Collection 中学过的:
add(E e)
remove(Object o)
contains(Object o)
size()
isEmpty()
clear()
addAll(...)
removeAll(...)
retainAll(...)
removeIf(...)
List 都可以继续使用。
在此基础上,List 又增加了:
基于索引的位置操作
这体现了接口继承关系:
Collection
↓
通用集合能力
List
↓
通用能力 + 位置能力
三、使用方法
3.1 创建 List
常用写法:
import java.util.ArrayList;
import java.util.List;
List<String> names =
new ArrayList<>();
左边:
List<String>
表示依赖 List 接口。
右边:
new ArrayList<>()
表示当前使用 ArrayList 实现。
后续也可以:
List<String> names =
new LinkedList<>();
这就是:
面向接口编程。
3.2 add(E e):末尾添加
从 Collection 继承:
boolean add(E e)
对于常用 List:
List<String> names =
new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
逻辑效果:
0 张三
1 李四
2 王五
默认:
add(element)
表示:
把元素追加到 List 末尾。
3.3 add(int index, E element):指定位置插入
List 特有的重要 API:
void add(
int index,
E element
)
作用:
在指定索引位置插入元素。
例如:
List<String> names =
new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
names.add(1, "赵敏");
插入前:
0 张三
1 李四
2 王五
插入:
index = 1
赵敏
插入后:
0 张三
1 赵敏
2 李四
3 王五
原来索引 1 及其之后的数据整体向后移动。
3.4 add(index, element) 的合法索引
假设:
list.size() == 3;
当前合法元素索引:
0
1
2
但插入时允许:
list.add(3, value);
因为:
在
size()位置插入,相当于追加到末尾。
因此 add(index, element) 的索引范围是:
0 <= index <= size()
注意最后是:
<= size()
3.5 get(int index):按照索引查询
方法:
E get(int index)
作用:
返回指定索引位置的元素。
例如:
List<String> names =
new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
String name =
names.get(1);
System.out.println(name);
得到:
李四
3.6 get 的索引范围
对于:
get(index)
合法范围:
0 <= index < size()
例如:
List<String> list =
new ArrayList<>();
list.add("Java");
list.add("MySQL");
合法:
list.get(0);
list.get(1);
错误:
list.get(2);
因为:
size = 2
合法索引只有:
0
1
此时可能抛出:
IndexOutOfBoundsException
3.7 set(int index, E element):修改元素
方法:
E set(
int index,
E element
)
作用:
使用新元素替换指定索引位置的旧元素。
例如:
List<String> names =
new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
String oldValue =
names.set(1, "赵敏");
修改前:
[张三, 李四, 王五]
修改后:
[张三, 赵敏, 王五]
返回值:
oldValue
是:
李四
所以:
set不仅完成修改,还返回被替换掉的旧元素。
3.8 remove(int index):按照索引删除
方法:
E remove(int index)
作用:
删除指定索引位置的元素,并返回被删除元素。
例如:
List<String> names =
new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
String removed =
names.remove(1);
删除:
李四
结果:
[张三, 王五]
同时:
removed
保存:
李四
3.9 remove(Object o):按照元素删除
List 同时还继承了:
boolean remove(Object o)
例如:
List<String> names =
new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("张三");
names.remove("张三");
它删除:
第一个匹配的
"张三"。
结果:
[李四, 张三]
因此 List 中存在两种非常重要的删除语义:
remove(index)
→ 根据位置删除
remove(object)
→ 根据元素匹配删除
3.10 List 的 remove 重载陷阱
这是 List 最经典的易错点之一。
例如:
List<Integer> numbers =
new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
现在:
numbers.remove(1);
到底表示:
删除数字 1
还是:
删除索引 1
答案是:
删除索引 1
因为调用匹配的是:
remove(int index)
所以删除:
20
结果:
[10, 30]
3.11 如果就是想删除 Integer 元素
假设:
List<Integer> numbers =
new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
希望删除:
20
可以:
numbers.remove(
Integer.valueOf(20)
);
此时参数类型是:
Integer
因此匹配:
remove(Object o)
表示:
按元素删除。
所以必须建立清晰判断:
remove(1)
1 是 int
→ remove(int index)
remove(Integer.valueOf(1))
参数是 Integer 对象
→ remove(Object o)
3.12 indexOf:查找第一次出现位置
List 还提供:
int indexOf(Object o)
作用:
返回指定元素第一次出现的索引。
例如:
List<String> names =
new ArrayList<>();
names.add("Java");
names.add("MySQL");
names.add("Java");
System.out.println(
names.indexOf("Java")
);
得到:
0
如果元素不存在:
names.indexOf("Redis");
返回:
-1
3.13 lastIndexOf:查找最后一次出现位置
方法:
int lastIndexOf(Object o)
例如:
[Java, MySQL, Java]
执行:
names.lastIndexOf("Java");
得到:
2
因此:
indexOf
→ 第一次出现
lastIndexOf
→ 最后一次出现
3.14 List 的普通索引 for
因为 List 有索引,所以可以:
for (int i = 0;
i < list.size();
i++) {
String element =
list.get(i);
System.out.println(element);
}
这是 Collection 通用体系中:
Set
通常无法使用的遍历方式。
因此 List 常见遍历方式包括:
普通索引 for
Iterator
增强 for
forEach
3.15 四种遍历方式
准备:
List<String> names =
new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
普通 for
for (int i = 0;
i < names.size();
i++) {
System.out.println(
names.get(i)
);
}
Iterator
Iterator<String> iterator =
names.iterator();
while (iterator.hasNext()) {
System.out.println(
iterator.next()
);
}
增强 for
for (String name : names) {
System.out.println(name);
}
forEach
names.forEach(
System.out::println
);
3.16 什么情况下需要索引 for
如果只想:
读取所有元素
增强 for 通常更简洁。
但是如果业务明确需要:
元素所在位置
普通索引 for 就非常自然。
例如:
for (int i = 0;
i < scores.size();
i++) {
System.out.println(
"第 " + (i + 1)
+ " 个成绩:"
+ scores.get(i)
);
}
3.17 JDK 21:getFirst 与 getLast
从 JDK 21 开始,List 继承了:
SequencedCollection
因此可以统一获取首尾元素。
例如:
List<String> technologies =
new ArrayList<>();
technologies.add("Java");
technologies.add("MySQL");
technologies.add("Redis");
System.out.println(
technologies.getFirst()
);
System.out.println(
technologies.getLast()
);
结果:
Java
Redis
以前通常写:
list.get(0);
以及:
list.get(
list.size() - 1
);
JDK 21 中:
getFirst();
getLast();
语义更加直接。
3.18 空 List 调用 getFirst/getLast
例如:
List<String> list =
new ArrayList<>();
list.getFirst();
List 当前为空。
此时会抛出:
NoSuchElementException
因此如果无法确认 List 非空,可以先:
if (!list.isEmpty()) {
System.out.println(
list.getFirst()
);
}
3.19 JDK 21:addFirst 与 addLast
List 还拥有:
addFirst(E e)
addLast(E e)
例如:
List<String> list =
new ArrayList<>();
list.add("MySQL");
list.addFirst("Java");
list.addLast("Redis");
结果:
[Java, MySQL, Redis]
对于 List:
addFirst(e)
可以从语义上理解为:
add(0, e)
而:
addLast(e)
可以理解为:
add(e)
3.20 JDK 21:removeFirst 与 removeLast
例如:
List<String> list =
new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
String first =
list.removeFirst();
String last =
list.removeLast();
最终 List:
[MySQL]
同时:
first = Java
last = Redis
这些方法主要改善的是:
首尾操作的统一 API 语义。
至于 ArrayList 和 LinkedList 执行这些操作时的性能差异,则取决于底层数据结构。
3.21 JDK 21:reversed()
List 还提供:
reversed()
用于获得:
反向顺序的 List 视图。
例如:
List<String> list =
new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
List<String> reversed =
list.reversed();
System.out.println(reversed);
逻辑上看到:
[Redis, MySQL, Java]
需要注意:
reversed()返回的是反向视图,不应简单理解成“创建了一个完全独立的新 ArrayList 副本”。
如果实现支持视图修改,修改可能写回原 List。
因此:
view
与:
copy
是不同概念。
四、原理与进阶
4.1 为什么 List 可以提供索引
List 的核心抽象是:
sequence(序列)。
集合中每个元素都有一个逻辑位置:
0
1
2
3
...
因此 List 能定义:
get(index)
set(index, element)
add(index, element)
remove(index)
但是:
List 接口规定“能够按照索引访问”,并不代表所有 List 实现访问索引的性能都一样。
例如:
ArrayList
和:
LinkedList
虽然都支持:
get(index)
但底层数据结构完全不同。
这会直接影响性能。
4.2 List 有索引,不等于所有 List 随机访问都快
这是一个非常重要的认识。
错误:
“List 有索引,所以
get(index)永远 O(1)。”
不是。
List 只规定:
你可以按照索引访问。
但具体实现方式由实现类决定。
例如:
ArrayList
→ 数组
LinkedList
→ 双向链表
因此:
同一个 List API
可能对应:
不同的数据结构
不同的时间复杂度
不同的性能特征
这也是为什么不能只学习接口 API,还必须学习实现类底层原理。
4.3 List 的相等性关注元素及顺序
List 的逻辑相等不仅关注:
有哪些元素
还关注:
元素出现的顺序
例如:
List A:
Java
MySQL
List B:
Java
MySQL
逻辑上可以相等。
但是:
List C:
MySQL
Java
虽然包含同样两个元素:
Java
MySQL
顺序不同,因此不能简单认为三个 List 完全相等。
这体现 List:
sequence
的语义。
4.4 ListIterator
除了普通:
Iterator
List 还提供更加专门的:
ListIterator
它能够:
向前遍历
向后遍历
获取迭代位置
在遍历中插入
替换元素
例如:
ListIterator<String> iterator =
list.listIterator();
可以:
iterator.hasNext();
iterator.next();
iterator.hasPrevious();
iterator.previous();
本阶段最重要的仍然是普通 List API。
但需要知道:
因为 List 具有明确序列位置,所以 Java 为它提供了更强的双向迭代器。
五、实践应用
5.1 教程章节列表
例如:
List<Chapter> chapters;
章节具有:
第 1 章
第 2 章
第 3 章
这样的明确顺序。
非常符合 List 模型。
5.2 博客最近更新
例如:
List<Article> recentArticles;
需要按照业务定义的顺序展示:
第一个
第二个
第三个
也适合 List。
5.3 购物车商品项
例如:
List<CartItem> items;
同一种商品是否可以出现多条,取决于业务设计,但数据本身天然具有列表结构。
5.4 学生成绩列表
List<Integer> scores;
例如:
95
78
95
60
成绩可以重复,因此 List 很自然。
六、常见问题
6.1 List 的“有序”是不是自动从小到大排序?
不是。
List 的“有序”主要表示:
元素具有明确的先后位置。
它不会因为:
list.add(100);
list.add(2);
list.add(80);
自动排序成:
2
80
100
排序需要额外操作。
6.2 List 一定允许重复吗?
JavaSE 入门通常概括为:
List 有序、可重复、有索引。
Java 标准常用实现:
ArrayList
LinkedList
都允许重复元素。
从接口规范的严谨角度看,实现可以增加额外限制,所以理解重点应该是:
List 的抽象并不要求元素必须唯一。
6.3 add(index, element) 为什么可以 index == size?
因为:
index == size
表示:
插入到当前最后一个元素之后。
也就是追加。
6.4 为什么 get(size()) 会报错?
假设:
size = 3
三个元素索引:
0
1
2
不存在:
3
所以:
get(size())
越界。
6.5 set 会改变 size 吗?
通常不会。
例如:
list.set(1, "Java");
只是:
把索引 1 的旧元素替换成新元素。
元素数量没有变化。
6.6 remove(index) 删除以后索引会怎样?
例如:
0 A
1 B
2 C
3 D
删除:
index = 1
结果:
0 A
1 C
2 D
后面的元素逻辑位置会向前移动。
具体底层如何移动,则取决于实现类。
6.7 List 中 remove(1) 为什么不是删除数字 1?
因为 Java 根据方法重载解析选择:
remove(int index)
参数:
1
是 int。
所以它代表索引。
如果要按值删除:
list.remove(
Integer.valueOf(1)
);
6.8 getFirst 和 get(0) 有什么区别?
对于非空 List:
getFirst()
逻辑上获取首元素。
get(0)
同样获取索引 0。
区别主要在于:
getFirst
→ 语义更加直接
get(0)
→ 明确使用索引
6.9 reversed() 会修改原 List 吗?
仅仅调用:
list.reversed();
不会把原 List 本身永久“翻转”。
它返回的是:
反向顺序视图。
不要和:
创建一个新的独立反向副本
混淆。
七、练习与验收
7.1 知识问答
- List 在 Java 集合体系中的定位是什么?
- 什么叫 List 的“有序”?
- 为什么“有序”不等于“排序”?
- 什么叫 List“可重复”?
- 什么叫 List“有索引”?
- List 的索引从几开始?
add(index, element)与add(element)有什么区别?get(index)做什么?set(index, element)返回什么?remove(index)返回什么?remove(Object)做什么?List<Integer>为什么特别容易出现 remove 重载陷阱?indexOf与lastIndexOf有什么区别?- List 为什么能使用普通索引 for?
- List 有索引是否意味着所有实现查询都同样快?
- JDK 21 中 List 为什么拥有
getFirst/getLast? reversed()返回的是副本还是视图?ListIterator相比普通 Iterator 增加了哪些能力?
7.2 代码阅读
阅读:
List<String> names =
new ArrayList<>();
names.add("A");
names.add("B");
names.add("C");
names.add(1, "X");
String old =
names.set(2, "Y");
String removed =
names.remove(0);
回答:
add(1, "X")后 List 是什么?set(2, "Y")修改了哪个元素?old保存什么?remove(0)删除什么?removed保存什么?- 最终 List 是什么?
阅读:
List<Integer> numbers =
new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.remove(1);
回答:
- 删除的是数字
1还是索引1? - 被删除元素是什么?
- 最终 List 是什么?
- 如果希望按值删除
20,应该如何表达调用意图?
7.3 手写代码
创建:
List<String> courses
依次完成:
- 添加
"JavaSE"。 - 添加
"MySQL"。 - 添加
"JavaWeb"。 - 在索引 1 插入
"Git"。 - 查询索引 2。
- 修改索引 0 为
"Java"。 - 删除索引 1。
- 查找
"JavaWeb"第一次出现的位置。 - 获取第一个元素。
- 获取最后一个元素。
- 获取反向视图。
- 使用普通 for 遍历。
7.4 Debug
代码:
List<String> list =
new ArrayList<>();
list.add("Java");
list.add("MySQL");
System.out.println(
list.get(list.size())
);
要求:
- 判断是否能够正常运行。
- 写出合法索引范围。
- 说明
size()与最大索引之间的关系。 - 修复代码。
代码:
List<Integer> data =
new ArrayList<>();
data.add(1);
data.add(2);
data.add(3);
data.remove(1);
需求实际上是:
删除数字
1。
要求:
- 判断当前代码实际删除什么。
- 解释方法重载原因。
- 修改代码实现按值删除。
7.5 综合训练
设计一个控制台课程管理列表:
JavaSE
MySQL
JavaWeb
Spring
要求支持:
添加课程
指定位置插入课程
根据索引查询
根据索引修改
根据索引删除
根据名称查找索引
显示第一门课程
显示最后一门课程
正序遍历
反向视图遍历
要求:
- 使用
List<String>作为变量类型。 - 使用 ArrayList 创建对象。
- 不使用 Stream。
- 所有索引操作必须检查边界。
7.6 本章验收
不查看资料,完成以下要求:
- 能够口述 List 的三个核心特点。
- 能够解释有序与排序的区别。
- 能够闭卷写出四个核心索引 API。
- 能够准确说出
set和remove(index)的返回值。 - 能够解释
add(index, e)为什么允许index == size()。 - 能够处理
List<Integer>的 remove 重载陷阱。 - 能够使用普通索引 for 遍历 List。
- 能够解释为什么 List 有索引但不同实现的索引访问性能可能不同。
- 能够使用 JDK 21 的
getFirst/getLast/reversed()。
如果仍然把:
list.remove(1);
一律理解为:
删除元素 1,
说明 List 的重载语义还没有真正掌握。