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

遍历是集合的最高频操作——本节把 7.2/7.3 散落的遍历知识收拢成体系:五种姿势一张表排开,ListIterator 补上"倒着走",forEach 请 4.7 的 Lambda 正式上岗,遍历中删除给出终极对比表。

7.4 集合遍历

1. 为什么单开一节

7.2 讲了增强 for 和迭代器三步舞,7.3 又加了普通 for——知识是散着来的。但遍历是集合用得最多的动作:查数据、改数据、筛数据、删数据,全都要"一个个走过去"。值钱的不光是"会遍历",而是:

本节把五张牌排开,然后专攻最难的"边遍历边删"。

2. 五种遍历姿势全景

姿势 适用范围 独门能力 何时用
普通 for List 专属(靠 get+size) 拿得到下标 i 要下标、要跳着取
增强 for 全家族通用 写法最短 只读遍历首选
迭代器 Iterator 全家族通用 it.remove() 安全删 遍历中删元素
列表迭代器 ListIterator List 专属 倒着走 + set 替换 + add 插入 从后往前、边走边改
forEach + Lambda 全家族通用 一行搞定(4.7 的 Lambda 上岗) 简单操作图省事
💡 决策口诀

只读增强 for · 要下标普通 for · 删元素迭代器 · 倒着走 ListIterator。口诀覆盖不了的场景极少,背这四句够用。

3. 迭代器的指针原理

7.2 说迭代器是"三步舞",现在看它内部到底在干嘛——迭代器内部有根指针,卡在元素之间(不是指着某个元素):

开始:  | A  B  C      (指针在最前)
next():  A | B  C      (取出 A,指针推过它)
next():  A  B | C      (取出 B)
next():  A  B  C |     (取出 C)
hasNext(): false       (后面没货了,结束)

两个方法各司其职:

由此得出两个新手坑:

⚠️ 坑一:连调两次 next() 会跳元素

System.out.println(it.next() + " " + it.next());——一次取 A 一次取 B,C 永远没人管。next() 一圈循环里只能出现一次,要用的元素先存变量。

⚠️ 坑二:不问 hasNext 直接 next()

没货可取抛 NoSuchElementException(运行时异常)。规矩永远是 while (it.hasNext()) 打头。

4. ListIterator:List 专属的增强版

Iterator 只能从头走到尾;ListIterator 是 List 的专属增强版——双向、能改、能插:

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

ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
    String s = lit.next();
    if (s.equals("B")) {
        lit.set("B2");            // 替换刚越过的元素(Iterator 没有 set)
    }
}
// 逆向再走一遍(此时指针已在末尾)
while (lit.hasPrevious()) {
    System.out.println(lit.previous());   // 倒着取
}
方法 作用 迭代器有吗
hasPrevious() / previous() 倒着问、倒着取 ✗ 独有
set(e) 替换刚越过的元素 ✗ 独有
add(e) 在指针位置插入 ✗ 独有(且不炸并发修改)
remove() 删除刚越过的元素 ✓ 同 Iterator
💡 previous 的前提

倒着走之前指针必须已经在尾部——先正向走到头(或用 listIterator(list.size()) 从尾部开局),previous 才有货。一上来就 previous 什么也取不到。

5. 遍历中删除:四种写法终极对比

7.2/7.3 两次埋的钩子,这里给出总账。需求:删掉集合里所有 "B"。

写法 结果 原因
① 增强 for 里 list.remove(s) ✗ 炸 ConcurrentModificationException 绕开迭代器改集合,账对不上
② 普通 for 正着删 ✗ 不炸,但漏删 删掉 i 后后面元素前顶,i++ 跳过了下一个
③ 普通 for 倒着删 ✓ 动的全是"走过"的位置,不会错位
④ 迭代器 it.remove() ✓ 迭代器自己删,账本同步
// ③ 倒着删(List 专属)
for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).equals("B")) {
        list.remove(i);
    }
}

// ④ 迭代器(全家族通用)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("B")) {
        it.remove();
    }
}

为什么会炸(账本机制):集合内部记着一本"结构修改次数"的账(modCount),迭代器领了一本自己的副本(expectedModCount)。每次 next() 前先核账:

倒着删为什么行:它根本不经过迭代器,是普通 for 直接用下标操作——没有账本可对,自然不炸。

💡 顺手记一个

Lambda 版 forEach 里删元素同样炸(它的底层也是迭代器)——forEach 只适合"看",不适合"删"。

6. forEach 与 Lambda:4.7 的 Lambda 正式上岗

4.7 学的函数式接口在这里第一次吃上正餐——forEach 的参数是个 Consumer(有且只有一个 accept 方法的函数式接口):

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

// 写法一:Lambda 完整版
list.forEach(s -> System.out.println(s));

// 写法二:方法引用(再省一步)
list.forEach(System.out::println);

对照迭代器三步舞:五行的 while 变成一行。限制:只有"看",拿不到下标,也不能删——要删回到第 5 节的两种正解。

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

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

实验 1:迭代器 while 里连写两次 it.next()——看 B 被跳过、循环只打了两次。改成先存变量再打印,恢复正常。

实验 2:三种删除法同台:增强 for 删(炸)→ 普通 for 正着删(加两个连续 "B",看漏删一个)→ 倒着删(全删干净)。漏删的现场比任何讲解都直观。

实验 3:ListIterator 正向走完用 set 改一个,再倒向打印——体会"指针在尾部才能 previous";再试一上来就 previous,看什么都打不出来。

实验 4:list.forEach(System.out::println) 一行遍历,再试试在里面删——亲眼看它也炸,印证"forEach 只能看"。

8. 常见误区

⚠️ 误区 1:一个 next() 当两个用

next() 是"取出并推进",连调两次就跳元素。循环体内要多次用某个元素,先 String s = it.next(); 存下来。

⚠️ 误区 2:正着 for 删元素"看起来也删了"

不炸但漏删——删 i 后 i+1 的元素顶到 i,i++ 直接跳过它。连续目标元素越多漏得越多。要删:倒着删或迭代器。

⚠️ 误区 3:ListIterator 一上来就 previous

指针开局在最前,previous 没货。先正向走到头,或 listIterator(list.size()) 从尾部开局。

⚠️ 误区 4:以为 ListIterator 的 set/add 会炸并发修改

不会——它是迭代器"自己改自己看的集合",账本自动同步。炸的只有绕开迭代器的 list.set/add/remove。

⚠️ 误区 5:以为 forEach 是什么新遍历机制

底层还是迭代器(for-each 化的 Lambda 语法糖)。它解决的是"写法长",不解决"删元素"——遍历中删除照样要走迭代器。

9. 自测一下

❓ Q1:迭代器的指针初始在哪?hasNext 和 next 各自做什么、动不动指针?

初始卡在第一个元素之前。hasNext 只问不看(指针不动);next 取出刚越过的元素并把指针推一格(动指针)。

❓ Q2:为什么循环里连调两次 next() 会出问题?正确姿势?

next() 每调一次指针推一格——连调两次跳过一个元素,还可能在末尾抛 NoSuchElementException。正确姿势:每次循环只调一次,先存变量。

❓ Q3:ListIterator 比 Iterator 多什么能力?

倒着走(hasPrevious/previous)、替换 set、插入 add——都是 List 专属(靠下标/双向链表结构才可能)。

❓ Q4:遍历中删元素的四种写法,各自结果与原因?

增强 for 删:炸(账对不上);正着 for 删:漏删(下标错位);倒着 for 删:✓(绕开账本);迭代器 it.remove():✓(两本账一起改)。

❓ Q5(毕业题):modCount 与 expectedModCount 的核对机制,解释为什么 list.remove() 炸而 it.remove() 不炸?

集合维护 modCount(结构修改次数),迭代器创建时拷贝一份为 expectedModCount,每次 next() 前核对。list.remove() 只改集合的 modCount,expected 还是旧值,下次 next() 核对失败抛 ConcurrentModificationException;it.remove() 删完同步改两本账,核对通过。

10. 下一步学什么


相关笔记