☕ 第七章 集合
📝 一句话概括

List = 家族里的点名册——存取顺序一致、每个位置有下标、允许重复。四个下标专属方法 + 普通 for 遍历都是它独有的;ArrayList 和 LinkedList 是同一合同的两个干法。

7.3 List接口

1. List:带下标的子合同

7.2 的家族地图里,List 是 Collection 底下的子合同——总合同的规矩它全守(add/remove/contains 都会),还额外加了一条:

必须有序、必须能按下标找人、允许重复。

Collection(总合同)
   ↑ extends
List(子合同:带下标的点名册)
   ↑ implements
ArrayList(数组干法)   LinkedList(链表干法)

「有序」的准确含义:存进去什么顺序,取出来还是什么顺序——不是按大小排序,是"排队"。

特点 含义 对照
有序 存取顺序一致 Set 就不保证(以后见)
带下标 每个位置有编号,从 0 起 和数组(2.7)一个规矩
可重复 同一元素能存多份 Set 到此为止

2. 四个下标专属方法(List 特有)

总合同里没有、只有 List 才有的操作:

List<String> list = new ArrayList<>();
list.add("波仔");
list.add("墨池");
list.add("AI");

list.add(1, "Java");        // 插队:下标 1 处插入,后面全体后挪
System.out.println(list.get(2));   // 墨池:按下标点名
list.set(0, "BoZai");       // 换人:下标 0 的内容替换
list.remove(2);             // 撤人:删下标 2 的元素
System.out.println(list.indexOf("AI"));  // 找人:返回首次出现的下标,没有返回 -1
方法 作用 备注
add(下标, e) 指定位置插入 原位置的元素及后面全体后挪
get(下标) 按下标取 越界抛 IndexOutOfBoundsException(老朋友)
set(下标, e) 按下标改 是"替换",不是"插入"
remove(下标) 按下标删 后面的全体前挪
indexOf(e) 按内容查下标 找不到返回 -1
💡 7.1 埋的 remove 歧义,这里分清

remove(1) 和 remove("波仔") 是重载(2.6 方法):参数是 int → 按下标删;参数是对象 → 按内容删(走 equals,7.2 的规矩)。看参数类型就能判断删的是什么。小心 List<Integer> 里的坑:remove(1) 删的是下标 1,不是值 1——想按值删要写 remove(Integer.valueOf(1))。

3. 三种遍历方式

List 有了下标,就多了一种 Collection 没有的遍历:

List<String> list = new ArrayList<>();
list.add("波仔"); list.add("墨池"); list.add("AI");

// 姿势一:普通 for —— List 专属(因为 get + size 都是下标玩法)
for (int i = 0; i < list.size(); i++) {
    System.out.println(i + " : " + list.get(i));
}

// 姿势二:增强 for —— 全家通用,只读场景首选
for (String s : list) {
    System.out.println(s);
}

// 姿势三:迭代器 —— 遍历中要删元素时的正解
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("AI")) {
        it.remove();        // 用迭代器的 remove,不用集合的
    }
}
💡 怎么选?

只读 → 增强 for;要下标或边遍历边改位置 → 普通 for;要边遍历边删 → 迭代器(it.remove())。

4. ArrayList 与 LinkedList:同一合同,两个干法

List 只是合同,干活的是实现类:

ArrayList LinkedList
底层 数组(2.7 那种连续格子) 双向链表(钩子串珠子)
查找 get(i) 快——下标直达 慢——从两头挨个走
中间增删 慢——后面全体挪动 快——摘钩子再挂钩子
头尾增删 尾部快,头部慢 两头都快

选型口诀:查得多、随机访问多 → ArrayList;头尾频繁增删 → LinkedList。新手默认无脑 ArrayList——绝大多数场景它就是最优解,LinkedList 认识即可。

💡 多态声明同样适用

List<String> list = new ArrayList<>();——左边写 List 合同,右边换实现只改一个词。7.2 的面向接口姿势在 List 这层同样成立。

5. 并发修改异常:7.2 的钩子正式收网

7.2 埋的那句"遍历中不能增删",现在讲清楚:

List<String> list = new ArrayList<>();
list.add("A"); list.add("B"); list.add("C");

for (String s : list) {           // 增强 for
    if (s.equals("B")) {
        list.remove(s);           // 炸!ConcurrentModificationException
    }
}

为什么炸:增强 for 底层是迭代器,迭代器自己记了一本"预期次数"的账;你绕过它直接改集合,账对不上,下次 next() 当场翻脸——这是运行时异常(5.2 收容所那派),编译器不拦。

正解(二选一):

// 解法一:迭代器 + it.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("B")) {
        it.remove();              // 迭代器自己删,账目一致,不炸
    }
}

// 解法二:普通 for 倒着删(List 专属福利)
for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).equals("B")) {
        list.remove(i);           // 倒着走,删掉后面的不影响前面的下标
    }
}

倒着删为什么行:正着删会把后面的元素往前顶、下标错位漏人;倒着删动的全是"已经走过"的位置,永远不会错位。

6. 动手试一试(30 秒实验)

新建 Demo03(import java.util.*;):

实验 1:add 三个元素后 list.add(1, "X"),打印——看"X 插队、后面后挪";再用 list.set(1, "Y") 对比——看 set 是替换不是插入。

实验 2:List<Integer> nums = new ArrayList<>(); 加 10、20、30,调 nums.remove(1) 再打印——看到删的是下标 1 的 20,不是值 1(想删值 1 得 remove(Integer.valueOf(1)))。

实验 3:增强 for 里 list.remove(s)——亲眼看 ConcurrentModificationException 爆出(和 7.2 实验 3 对照)。

实验 4:同一个删除需求分别用迭代器 it.remove() 和普通 for 倒着删——两种正解都跑通,再体会正着删为什么会漏人(add 两个连续目标试试)。

7. 常见误区

⚠️ 误区 1:以为 remove(1) 一定删"值为 1 的元素"

看参数类型:int → 按下标;对象 → 按内容。List<Integer> 里这个歧义是真会咬人的,删值要 remove(Integer.valueOf(1))。

⚠️ 误区 2:set 当插入用

set(下标, e) 是替换该位置,元素个数不变;插入要用 add(下标, e)。

⚠️ 误区 3:普通 for 遍历中正着 remove/add

下标错位:删掉 i 后,原 i+1 顶到 i,i++ 直接跳过了它——漏人。要删就倒着循环或用迭代器。

⚠️ 误区 4:以为 List 的"有序"是按大小排序

有序 = 存取顺序一致(排队),不是 sorted(按值排列)。想按值排序是以后 Arrays.sort / 集合排序的事。

⚠️ 误区 5:LinkedList 一定比 ArrayList 慢

不成立。头尾增删 LinkedList 更快;中间插入它还得先"走到"那个位置(查找慢),不一定占便宜。别背"谁快谁慢"的口诀,记住底层是数组还是链表,自己推。

8. 自测一下

❓ Q1:List 相比 Collection 多了哪三大特点?

有序(存取顺序一致)、带下标(按下标操作)、可重复。三个都是 Collection 合同里没有的。

❓ Q2:`list.remove(1)` 和 `list.remove("波仔")` 分别走哪个方法?依据是什么?

前者按下标(参数 int),后者按内容(参数对象,走 equals)。依据是重载(2.6 方法)按参数类型区分。

❓ Q3:`List` 里想删"值为 1 的元素",为什么直接 `remove(1)` 不行?

remove(1) 匹配的是 int 参数版 = 按下标删。要删值 1 必须 remove(Integer.valueOf(1))——让参数变成 Integer 对象,走按内容版。

❓ Q4:增强 for 遍历中删元素为什么抛 ConcurrentModificationException?两种正解是什么?

增强 for 底层是迭代器,迭代器记着预期修改次数;绕过它改集合账目对不上,next() 时翻脸(运行时异常)。正解:① 迭代器 it.remove();② 普通 for 倒着删。

❓ Q5(毕业题):ArrayList 和 LinkedList 底层各是什么?"频繁在头部插入元素"选谁,为什么?

ArrayList 底层是数组,LinkedList 底层是双向链表。头部插入选 LinkedList:链表挂钩子即可;ArrayList 头部插入要把全部元素整体后挪,元素越多越慢。(但注意:如果插入前还要先查找,LinkedList 找位置也慢,实战仍多为 ArrayList。)

9. 下一步学什么


相关笔记