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 知识问答

  1. List 在 Java 集合体系中的定位是什么?
  2. 什么叫 List 的“有序”?
  3. 为什么“有序”不等于“排序”?
  4. 什么叫 List“可重复”?
  5. 什么叫 List“有索引”?
  6. List 的索引从几开始?
  7. add(index, element)add(element) 有什么区别?
  8. get(index) 做什么?
  9. set(index, element) 返回什么?
  10. remove(index) 返回什么?
  11. remove(Object) 做什么?
  12. List<Integer> 为什么特别容易出现 remove 重载陷阱?
  13. indexOflastIndexOf 有什么区别?
  14. List 为什么能使用普通索引 for?
  15. List 有索引是否意味着所有实现查询都同样快?
  16. JDK 21 中 List 为什么拥有 getFirst/getLast
  17. reversed() 返回的是副本还是视图?
  18. 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);

回答:

  1. add(1, "X") 后 List 是什么?
  2. set(2, "Y") 修改了哪个元素?
  3. old 保存什么?
  4. remove(0) 删除什么?
  5. removed 保存什么?
  6. 最终 List 是什么?

阅读:

List<Integer> numbers =
        new ArrayList<>();

numbers.add(10);
numbers.add(20);
numbers.add(30);

numbers.remove(1);

回答:

  1. 删除的是数字 1 还是索引 1
  2. 被删除元素是什么?
  3. 最终 List 是什么?
  4. 如果希望按值删除 20,应该如何表达调用意图?

7.3 手写代码

创建:

List<String> courses

依次完成:

  1. 添加 "JavaSE"
  2. 添加 "MySQL"
  3. 添加 "JavaWeb"
  4. 在索引 1 插入 "Git"
  5. 查询索引 2。
  6. 修改索引 0 为 "Java"
  7. 删除索引 1。
  8. 查找 "JavaWeb" 第一次出现的位置。
  9. 获取第一个元素。
  10. 获取最后一个元素。
  11. 获取反向视图。
  12. 使用普通 for 遍历。

7.4 Debug

代码:

List<String> list =
        new ArrayList<>();

list.add("Java");
list.add("MySQL");

System.out.println(
        list.get(list.size())
);

要求:

  1. 判断是否能够正常运行。
  2. 写出合法索引范围。
  3. 说明 size() 与最大索引之间的关系。
  4. 修复代码。

代码:

List<Integer> data =
        new ArrayList<>();

data.add(1);
data.add(2);
data.add(3);

data.remove(1);

需求实际上是:

删除数字 1

要求:

  1. 判断当前代码实际删除什么。
  2. 解释方法重载原因。
  3. 修改代码实现按值删除。

7.5 综合训练

设计一个控制台课程管理列表:

JavaSE
MySQL
JavaWeb
Spring

要求支持:

添加课程
指定位置插入课程
根据索引查询
根据索引修改
根据索引删除
根据名称查找索引
显示第一门课程
显示最后一门课程
正序遍历
反向视图遍历

要求:

  • 使用 List<String> 作为变量类型。
  • 使用 ArrayList 创建对象。
  • 不使用 Stream。
  • 所有索引操作必须检查边界。

7.6 本章验收

不查看资料,完成以下要求:

  • 能够口述 List 的三个核心特点。
  • 能够解释有序与排序的区别。
  • 能够闭卷写出四个核心索引 API。
  • 能够准确说出 setremove(index) 的返回值。
  • 能够解释 add(index, e) 为什么允许 index == size()
  • 能够处理 List<Integer> 的 remove 重载陷阱。
  • 能够使用普通索引 for 遍历 List。
  • 能够解释为什么 List 有索引但不同实现的索引访问性能可能不同。
  • 能够使用 JDK 21 的 getFirst/getLast/reversed()

如果仍然把:

list.remove(1);

一律理解为:

删除元素 1,

说明 List 的重载语义还没有真正掌握。