做Java开发这些年陆陆续续带过不少新人。几乎每个刚入门的同学都会在集合遍历上栽几个跟头要么在循环里删元素删出并发修改异常要么拿到HashMap不知道怎么下手要么在面试时被问住这两种遍历到底有什么区别。其实Java集合遍历就三板斧for循环、增强for、Iterator迭代器。把这三大遍历技巧吃透了List、Set、Map这些集合的操作就会顺很多面试里那些高频的遍历八股也基本都能拿捏住。这篇文章我按我自己带新人的思路来写从原理到实战、从写法到坑点、再到面试答辩争取让你看完就能直接上手。1. 为什么遍历是个绕不开的话题1.1 一个面试老问题背后的真实需求先聊聊遍历这个词。说白了就是把集合里的元素一个一个拿出来处理。你写业务代码时要统计订单金额要遍历订单列表要筛选用户信息要遍历用户集合要对HashMap里的配置做批量操作还是遍历。可以说集合操作里八成以上的代码都和遍历有关。面试官爱问HashMap的遍历方式都有哪些问增强for和Iterator有什么区别这些问题看着像背八股实际考察的是你有没有真正理解集合的存储结构。List是有序的、有下标的所以能用for循环Set是无序的、元素唯一的压根没有下标这一说就只能靠迭代器或者增强forMap是键值对映射它有自己的一套Entry概念遍历方式和Collection体系又不一样。理解了这个背景你就知道为什么同一个Java里会有好几种遍历姿势——不是语言设计者闲得慌而是不同数据结构的访问方式本来就不同每种遍历技巧都有它不可替代的使用场景。1.2 集合框架的底层本质结构决定了遍历方式Java的集合框架大体分两大体系Collection和Map。Collection下面又分List、Set、Queue。List家族里有ArrayList和LinkedList两个主力军。ArrayList底层是数组内存连续按下标取值是O(1)的随机访问LinkedList底层是双向链表要找第N个元素只能从头部或尾部一个个走过去时间复杂度是O(N)。这一条差异直接决定了传统for循环在这两种List上表现天差地别。Set家族里HashSet底层依赖HashMap无序且不可重复根本没有下标概念TreeSet底层是红黑树有排序但是也没有下标。它们想遍历只能靠迭代器或增强for。Map家族里HashMap底层是数组加链表加红黑树有Hash桶的概念它遍历的时候拿到的是一组一组的Entry也就是键值对这个完整对象而不是单独一个值。我经常拿购物车打比方ArrayList就像一排带编号的货架你报个号就能直接走到对应位置拿东西LinkedList像一条项链你想拿第10颗珠子得从两头一颗一颗数过去HashMap像一个按标签分类的储物柜你打开柜子看到的是一个写着标签-物品的配对卡片。数据结构的物理形态决定了你能怎么去访问它这就是为什么同一件事会有好几种做法。2. 逐个拆解三大遍历技巧写法、原理、坑点2.1 传统for循环最直白但只认下标传统for循环大家最早接触写法也最简单ListString list new ArrayList(Arrays.asList(a, b, c)); for (int i 0; i list.size(); i) { System.out.println(位置 i : list.get(i)); }它的核心逻辑就是我按位置来访问。优势非常明显你能拿到当前下标能在遍历过程中根据位置做判断能自由控制步长比如隔一个取一个也能从后往前倒序遍历。在需要修改某个位置元素的时候它是最直接的方式for (int i 0; i list.size(); i) { if (list.get(i).equals(b)) { list.set(i, B); } }但这个写法有个隐藏的大坑也是新人最容易踩的循环体里直接remove。如果你用for循环删除元素删除后后面的元素会自动前移下标就乱了。比如有[a,b,c,d]你打算删掉所有等于b的元素从i0开始删完b之后c移到下标1此时i自增变成2c就被跳过去了。这就是经典的删除错位问题。正确做法是删除后手动把i减回去for (int i 0; i list.size(); i) { if (list.get(i).equals(b)) { list.remove(i); i--; // 删除后下标前移必须回退一位 } }或者干脆倒序遍历从尾部往头部删因为删除后面的元素不影响前面元素的下标这个技巧在很多算法题里都有用比如力扣上那些删除列表中元素的题目。另外一点要特别提醒传统for循环跟LinkedList的兼容性很差。如果你拿一个几万元素的LinkedList去做list.get(i)每取一个元素都要从链表头或尾走一遍这等于for循环里套了一个O(N)查找整体直接变成O(N²)级别数据量一上来就跑得想砸电脑。所以判断要不要用for循环先看清楚集合底层是数组还是链表。2.2 增强for循环语法糖的甜与苦增强for是Java 5开始支持的写法for (String s : list) { System.out.println(s); }它是专门给只想老老实实把每个元素看一遍的场景设计的。写起来极其简洁不用管下标不用关心集合长度编译器会自动帮你处理迭代逻辑。实际在编译的时候增强for会被转换成一个基于Iterator的循环你可以把它理解成语法糖。也就是说增强for底层走的还是迭代器那一套。用增强for有个非常舒服的地方是它适用于所有Iterable接口的实现类不管是ArrayList还是HashSet不管有没有下标通通一个写法搞定。对新手来说这是最不容易出错的遍历方式。但它的甜也是有代价的。最大的坑就是遍历过程中不能对集合做结构性修改。所谓结构性修改就是增加元素、删除元素这类改变集合大小或结构的操作。下面的代码会抛ConcurrentModificationException:ListString list new ArrayList(Arrays.asList(a, b, c)); for (String s : list) { if (s.equals(b)) { list.remove(s); // 运行时抛异常 } }原因我后面专门讲简单说就是迭代器会检查集合的修改次数发现你在遍历期间偷偷动了集合立刻扔异常出来这个机制叫fail-fast快速失败机制。它的设计初衷就是如果数据在遍历中被改了那后面拿到的数据可能就错了与其让你用错误数据还不如直接告诉你出问题了。除此之外增强for拿不到当前下标。如果确实需要可以自己维护一个计数器但这样写就有点绕了这种场景我更建议直接用传统for。2.3 Iterator迭代器唯一能在遍历中安全删元素的Iterator是Collection体系里最通用的遍历工具。别看它接口就三个核心方法但它在面试和实际开发中的地位非常高IteratorString iterator list.iterator(); while (iterator.hasNext()) { String s iterator.next(); System.out.println(s); }hasNext()判断还有没有下一个next()取出下一个元素两个方法配合就能把一个集合从前往后走完。Iterator的核心价值在于它提供了一个不依赖下标的通用遍历协议只要是Collection的子类都能用同一套方式遍历这就把ArrayList和HashSet统一起来了。最关键的一点是Iterator的remove()方法可以在遍历过程中安全地删除当前元素。为什么说安全因为迭代器内部会维护一个expectedModCount它自己调用remove()的时候会同步更新这个值所以不会触发modCount校验失败。但需要注意必须先next()再remove()也就是说只能删除刚取出来的那一个不能上来就remove。IteratorString iterator list.iterator(); while (iterator.hasNext()) { String s iterator.next(); if (s.equals(b)) { iterator.remove(); // 安全删除当前元素 } }这个场景在实战中非常常见比如批量清理不符合条件的日志、过滤掉黑名单用户等。如果你在面试中被问到遍历时怎么删除元素答案就是迭代器的remove方法或者JDK 8的removeIf千万别答在for循环里调用list.remove那是典型的坑人写法。另外ListIterator是Iterator在List上的增强版它多了previous()方法可以从后往前遍历还支持add()和set()方法在遍历时插入和修改元素。但实际业务里用到它的频率不算高了解即可。2.4 Map的遍历面试里问得最多的那张表Map不属于Collection体系但它同样有遍历的需求而且面试里问得比List还多。HashMap至少有四种常见遍历姿势我按推荐程度来排。第一种entrySet加增强for。这是最推荐的通用写法遍历时直接拿到完整的Entry对象同时包含键和值不需要再单独查一次MapMapString, Integer map new HashMap(); map.put(a, 1); map.put(b, 2); for (Map.EntryString, Integer entry : map.entrySet()) { System.out.println(entry.getKey() entry.getValue()); }第二种keySet加get。这种写法是先拿到所有键的集合再逐个根据键去取对应的值for (String key : map.keySet()) { System.out.println(key map.get(key)); }它的缺点是每拿到一个key都要通过get再查一遍HashMap等于多了一次哈希查找。数据量小的时候没感觉一旦是几十万条的大Map这个开销就非常明显。所以常规场景我建议优先用entrySet。第三种values()只遍历值。如果你只关心值不关心键可以直接for (Integer value : map.values()) { System.out.println(value); }这个就没什么好说的用的时候要知道它拿不到键。第四种JDK 8的forEach写法。一行搞定阅读性也最好map.forEach((key, value) - { System.out.println(key value); });这个写法底层也是基于entrySet的迭代只是语法上更简洁。但要注意这个forEach里面同样不能做put或者remove这种结构性修改。Map的遍历还有一个细节HashMap本身是无序的遍历顺序不保证和插入顺序一致。如果业务上要求遍历顺序跟插入顺序一致要用LinkedHashMap要求按键排序要用TreeMap。很多人在遍历Map时发现顺序不对其实不是代码写错了是容器选错了。3. 三大遍历的对比和选型3.1 能不能删、能不能改、能不能知道位置我从实际开发的角度把三大遍历的适用边界整理成一个对比表面试前也好用来回顾维度传统for循环增强for循环Iterator迭代器是否支持按下标访问支持直接get(i)不支持拿不到下标不支持只能顺序取能否修改元素内容能set(i, xxx)能但需要重新赋值给引用能ListIterator支持set能否删除当前元素能但容易下标错乱需要i--不能会抛并发修改异常能迭代器remove()安全能否从后往前遍历能倒序i--不能ListIterator支持previous()适用集合只有有下标的List所有Collection所有Collection这个表看着简单但能把背后逻辑说出来的人不多。传统for循环的不能随便删问题本质是删除后元素位移导致下标错位增强for的不能删本质是fail-fast机制在保护你Iterator能安全删是因为它自己内部同步了修改计数。面试时你能把这三句话说出来就和纯背答案的候选人拉开差距了。还有一个高频细节是遍历时到底能不能修改元素内容。很多人把修改元素内容和结构性修改混在一起。增强for里你拿到的是集合元素的引用如果你修改的是List中的某个对象内部的属性比如user.setName(新名字)这样完全没问题但如果你执行list.add或list.remove这就是结构性修改会影响迭代器的遍历状态所以会被拦截。这个边界搞清楚了写代码的时候就能少踩很多莫名其妙的坑。3.2 集合类型不一样遍历姿势也得变选遍历方式不能只看写法顺不顺手还得看集合的底层结构。ArrayList这种基于数组的List三种遍历方式都适用性能上也都很不错区别只在于你需不需要下标、需不需要删除。LinkedList数组结构的List我强烈建议别用传统for循环疯狂get换成增强for或者Iterator。Set接口的集合没有下标也没有get方法增强for和Iterator基本就是标配想用传统for反而用不了。Map则要看你是要键、要值、还是要键值对业务上需要什么就选对应的遍历入口。我实际写代码时的选择逻辑很简单如果只是想把每个元素看一遍做点处理优先增强for如果处理过程中要安全删除元素直接用Iterator如果必须知道下标位置或者要控制遍历步长用传统for遇到Map无脑选entrySet或者Java 8的forEach不要为了省那一行代码用keySet去多查一次map。按这个逻辑走基本不会出大问题。4. 常见坑与面试避雷指南4.1 ConcurrentModificationException是怎么来的这是集合遍历里出现频率最高的异常同时也是面试问fail-fast机制最常见的切口。它的来历要深入到ArrayList源码里的两个变量去看modCount和expectedModCount。modCount是集合自己的修改计数器每次对ArrayList做add、remove这样的结构性修改modCount都会加1。expectedModCount是迭代器内部保存的一个副本在创建迭代器的时候expectedModCount被初始化成当时的modCount。之后每一次调用next()迭代器都会检查modCount和expectedModCount是否一致。如果发现不一致说明在你遍历期间有人动了集合结构真实的迭代状态已经不可靠了于是果断抛出ConcurrentModificationException。这就是fail-fast快速失败。它不像有些人以为的那样是多线程才有的事单线程下你增强for里remove元素一样会触发这个异常。它的存在是为了防止你使用一个已经脏掉的迭代结果继续做业务宁可直接报错也不要给你一个半真半假的数据让你稀里糊涂地跑下去。和fail-fast相对的是fail-safe。Java并发包里的CopyOnWriteArrayList就是典型代表它遍历时基于一个副本数组修改操作改的是新数组所以迭代器不会抛ConcurrentModificationException。坏处是它牺牲了数据的实时性遍历过程中拿不到最新的修改。这个对比也是面试常客理解了原理就很容易记住。4.2 空指针和循环内remove的翻车现场除了并发修改异常还有几个实际开发里反复出现的问题我一个个说。第一个是空指针。对null集合做增强for循环会直接抛NullPointerException。很多人在从数据库查数据、从接口返回结果的时候没有做空集合判断就直接遍历。建议养成习惯拿到的集合先判空再遍历或者用工具类判空比如ListUtils.isEmpty()这类方法避免在遍历这一步碰上空指针。第二个是迭代器删除时忘记调用next()。Iterator定义的是有了next才能remove你在remove之前必须先next()。有的同学遍历时想删一个结果逻辑写在hasNext()判断后、next()调用前直接remove就会抛IllegalStateException。Iterator的原理是每次remove都删除最近一次next返回的那个元素没有next过自然没有元素可删。第三个是循环里remove时下标忘了减。我前面说过for循环删除后有元素前移不处理下标会跳过元素。这里最经典的场景是过滤集合里的某些值比如你想把所有空字符串都删掉结果删一个跳一个最后剩下b、d这种漏网之鱼调试半天才发现是下标问题。第四个是Map遍历中做put或remove直接抛异常。Map的forEach和增强for遍历一样遍历期间对Map做结构修改同样会触发ConcurrentModificationException。业务上常见的遍历Map时想删掉若干键值对正确写法是用iteratorIteratorMap.EntryString, Integer it map.entrySet().iterator(); while (it.hasNext()) { Map.EntryString, Integer entry it.next(); if (entry.getValue() 0) { it.remove(); } }4.3 面试怎么答8个高频问题速查结合我和候选人面谈的经验把集合遍历这块面试官最可能问的问题整理成了一份速查表每个问题我都给了参考回答思路。问题关键回答要点HashMap的遍历方式有哪些至少说四种entrySet、keySet、values、JDK 8的forEach然后说推荐entrySet因为一次拿到键值对避免二次查表增强for和Iterator有什么区别增强for是语法糖底层就是Iterator增强for不能删除元素Iterator可以安全删除迭代器遍历时能不能删除元素能必须先next()再remove()原理是迭代器会同步expectedModCountfail-fast和fail-safe是什么fail-fast是在遍历中发现集合被修改就抛异常fail-safe是在副本上遍历不抛异常但读不到实时数据ArrayList和LinkedList遍历性能差异ArrayList按下标随机访问快LinkedList用下标访问慢适合迭代器顺序遍历forEach里能不能remove/put不能会触发ConcurrentModificationException遍历时修改元素内容算不算修改修改对象内部属性不算结构性修改只有add/remove这类改变集合大小或结构的才算JDK 8的forEach和传统遍历的关系forEach是entrySet遍历的简化写法本质上还是迭代器循环回答这些问题的核心思路是先说清楚是什么再说清楚为什么。比如问fail-fast你说它是在集合被修改时抛异常加分项是补充一句标记-检查机制用modCount和expectedModCount比较来判断集合是否被修改。这就是知其所以然和背答案的区别。5. 进阶补充从遍历到工程实战5.1 遍历和删除的新工具removeIf与StreamJDK 8之后如果你只是想遍历集合并删除符合某些条件的元素其实有更省事的写法用Collection默认方法removeIf一行搞定list.removeIf(s - s.startsWith(b));removeIf的设计思路很巧妙它内部就是基于迭代器遍历并调用remove()实现的所以完全安全不用你自己写Iterator那一套循环。这个写法在代码review的时候观感特别好比一堆whileifiterator简洁太多。Map的批量删除可以配合entrySet的removeIfmap.entrySet().removeIf(entry - entry.getValue() 0);它同样是一行搞定。如果你需要更复杂的过滤逻辑那再考虑Stream的filter它会返回一个新的集合不影响原数据。但工程上有一个取舍Stream的方式会产生新集合内存开销更大removeIf是原地过滤不产生新集合适合处理大列表。我在实际项目里删除场景多数优先用removeIf只有需要同时做其他转换和收集时才上Stream。5.2 一个实战自查清单给正在学集合遍历的同学一个自查清单每一条都是我或者同事在项目里真实踩过坑换回来的第一拿到集合先问自己这个集合允许重复吗有序吗能按下标访问吗根据答案决定遍历方式而不是一个增强for走天下。第二遍历过程中要删除元素先把自己从用for循环remove的惯性思维里拉出来改成Iterator或者removeIf。第三遍历Map优先entrySet或forEach想删键值对就用entrySet的iterator。第四任何遍历代码都要考虑集合为null的情况先判空再循环。第五涉及大集合的遍历注意集合底层是数组还是链表选错遍历方式性能能差出几个数量级。最后再分享一个我自己实战中的判断标准遍历代码写完以后看一眼循环体。如果循环体里出现list.add、list.remove、list.clear这类改变集合结构的操作立刻打住基本可以确定写法有隐患换成迭代器或者removeIf才是正解。这个习惯我保持了很多年也带着组里的新人一直在这么做基本能避开九成以上的遍历类线上bug。集合遍历这件事看起来基础得不能再基础但越基础的东西越容易被人忽略细节。我见过太多工作两三年的人还在用错误的删除方式拿ArrayList做基底遍历也见过不少面试者在一道简单HashMap遍历方式问题上翻车。说真的把三大遍历技巧的代码细节和底层原理吃透你的Java基础就稳了一大半。写代码这件事没有捷径但把这些地基打扎实了后面学并发、学框架、看源码都会顺畅很多。