遍历是集合的最高频操作——本节把 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 (后面没货了,结束)
两个方法各司其职:
hasNext()只问不看:指针后面还有货吗?——不移动指针next()又取又推:取出指针刚越过的元素,并把指针再往前推一格
由此得出两个新手坑:
System.out.println(it.next() + " " + it.next());——一次取 A 一次取 B,C 永远没人管。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 |
倒着走之前指针必须已经在尾部——先正向走到头(或用 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() 前先核账:
it.remove():迭代器删完顺手把两本账一起改——核账通过 ✓list.remove():只改了集合那本——迭代器下次核账发现对不上,当场抛 ConcurrentModificationException(运行时异常那派,编译不拦)
倒着删为什么行:它根本不经过迭代器,是普通 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. 常见误区
next() 是"取出并推进",连调两次就跳元素。循环体内要多次用某个元素,先 String s = it.next(); 存下来。
不炸但漏删——删 i 后 i+1 的元素顶到 i,i++ 直接跳过它。连续目标元素越多漏得越多。要删:倒着删或迭代器。
指针开局在最前,previous 没货。先正向走到头,或 listIterator(list.size()) 从尾部开局。
不会——它是迭代器"自己改自己看的集合",账本自动同步。炸的只有绕开迭代器的 list.set/add/remove。
底层还是迭代器(for-each 化的 Lambda 语法糖)。它解决的是"写法长",不解决"删元素"——遍历中删除照样要走迭代器。
9. 自测一下
初始卡在第一个元素之前。hasNext 只问不看(指针不动);next 取出刚越过的元素并把指针推一格(动指针)。
next() 每调一次指针推一格——连调两次跳过一个元素,还可能在末尾抛 NoSuchElementException。正确姿势:每次循环只调一次,先存变量。
倒着走(hasPrevious/previous)、替换 set、插入 add——都是 List 专属(靠下标/双向链表结构才可能)。
增强 for 删:炸(账对不上);正着 for 删:漏删(下标错位);倒着 for 删:✓(绕开账本);迭代器 it.remove():✓(两本账一起改)。
集合维护 modCount(结构修改次数),迭代器创建时拷贝一份为 expectedModCount,每次 next() 前核对。list.remove() 只改集合的 modCount,expected 还是旧值,下次 next() 核对失败抛 ConcurrentModificationException;it.remove() 删完同步改两本账,核对通过。
10. 下一步学什么
- ☑ 7.5 Set接口 ✅ —— 已完成:Set vs List 三大差异、HashSet 去重原理(hashCode 分桶 + equals 验身)、4.5 搭伴规矩正式收网、自定义类去重实战。点此复习
相关笔记
- 第七章 集合/7.3 List接口 ← 上一篇:List 三大特点与下标操作,本节专攻遍历
- 第七章 集合/7.2 Collection接口:迭代器三步舞首次登场,本节拆开看内部
- 第四章 面向对象(下)/4.7 函数式编程:Lambda 正式上岗(forEach 的参数是函数式接口)
- 第五章 异常/5.2 运行时异常与编译时异常:ConcurrentModificationException 与 NoSuchElementException 都属运行时那派
- 第四章 面向对象(下)/4.5 Object类:hashCode 搭伴规矩——下一节 HashSet 收网