尧图精选

Java集合遍历全解析:从for到Stream的底层原理与实战避坑

🕒 发布时间:2026/10/2 4:24:18 📁 来源:尧图网络
做了十几年Java开发面试过的人也快三位数了几乎每次都会问到遍历相关的问题。很多人觉得遍历太简单就是“一个一个拿出来嘛”。但真正往深了问能把所有遍历方式、底层原理、坑点利弊说清楚的人少之又少。尤其这些年Java 8的函数式风格普及以后forEach、Stream一上来很多新人就只记住了一种写法集合里删除元素翻车、遍历大集合性能踩坑的情况屡见不鲜。这篇文章就集中把Java里遍历集合、数组、Map乃至树结构的各种方法理一遍每种方式的底层机制、适用场景、优缺点我都会拆开讲哪些是面试必考的底层逻辑哪些是实际项目中容易踩的坑都会标注清楚。不管你是刚开始学Java的新手还是准备跳槽的面试党又或者单纯想把自己的代码写得更稳的老手都能在这篇里找到值得细看的内容。1. 遍历的基本盘——三类基础写法各自的脾气1.1 普通for循环最原始但最灵活的存在普通for循环可能是很多人接触到的第一种遍历方式它的核心逻辑就是通过索引下标去访问容器里的元素。这里必须首先说明一个很多新手容易混淆的关键点不是所有数据结构都能用普通for循环高效遍历。拿ArrayList举例它的底层是一个动态数组内存空间是连续的所以通过下标访问元素的时间复杂度是O(1)随机访问性能极强。而LinkedList就完全不是一回事了它是一个双向链表每个节点只记录上一个和下一个节点的地址通过下标的get(int index)操作其实需要从链表的头部或者尾部逐个往后找时间复杂度为O(n)。如果在LinkedList上用普通for循环做遍历那实际的总消耗就是O(n²)——外循环n次每次get又要内部循环n次数据量大了之后这种写法就是灾难。普通for循环的核心代码如下ListString list new ArrayList(); list.add(java); list.add(遍历); list.add(方法); for (int i 0; i list.size(); i) { String item list.get(i); System.out.println(item); }这段代码看起来简单但它的优点是绝对灵活的拿到索引i之后可以做双向遍历从头到尾、从尾到头可以控制步长隔一个取一个可以在循环内部修改某个位置的元素甚至可以精确跳过某些元素。它的缺点也很明显代码不够简洁每次都要自己维护索引变量而且如前面说到的如果容器不是随机访问型结构性能会非常难看。我见过不少开发者在处理LinkedList时还用这种方式数据量几千条或许没感觉一旦到了十万级以上操作延迟会直接飙升到让人怀疑是服务器出了问题。这种问题的排查方式其实也很简单——把这个for循环换成增强for性能立刻恢复原因就在于底层换了遍历机制。1.2 增强for语法糖背后的迭代器真相增强for循环也就是foreach写法是JDK 5引入的。注意这个“foreach”本身并不是Java的关键字它只是一种语法糖。写起来非常舒服ListString list Arrays.asList(java, 遍历, 方法); for (String item : list) { System.out.println(item); }很多人以为这只是在普通for外面包了一层壳用起来顺手而已。但如果在IDE里把这段代码反编译比如用IDEA的Show Bytecode或者javap -c命令去查看字节码你会发现编译后的代码实际上创建了一个Iterator对象然后用它来逐个访问元素。也就是说增强for就是Iterator模式的一个便捷写法。既然本质是Iterator那么它就有两个天然的限制。第一是循环体内不能直接修改集合结构——比如调用add或者remove否则大概率触发ConcurrentModificationException这一点下面第3章会展开讲。第二是它拿不到当前遍历到的索引下标所以如果只是要把每个元素取出来处理一下增强for是最佳选择但如果你需要“修改某个特定位置的元素”增强for就力不从心了还得回到普通for。展开来说增强for在遍历数组时又略有不同如果遍历的是数组编译后底层就是普通的下标循环因为数组压根没有Iterator的概念。这一点很多人在细节上说不清楚面试时如果被问到“增强for遍历数组和遍历集合的字节码区别”能答出这个层面的基本就算合格了。1.3 Iterator迭代器遍历集合的“底层正规军”Iterator是Java集合框架的根接口之一定义了一个统一的遍历协议。它有三个核心方法hasNext()判断是否还有下一个元素next()取出当前元素并让指针后移remove()移除当前迭代器最后一次返回的元素。几乎所有Collection的子类都实现了这个接口只是各自内部的数据结构不同迭代的顺序和效率也不同。用Iterator显式遍历的代码如下ListString list new ArrayList(); IteratorString it list.iterator(); while (it.hasNext()) { String item it.next(); System.out.println(item); }它和增强for最大的区别在于增强for不允许在循环体内修改集合结构但Iterator的remove方法本身就是设计出来支持安全删除的。除此之外Iterator还支持迭代器模式的扩展比如并发包里的ListIterator、延迟加载的数据流等很多场景底下层框架都是通过Iterator把遍历逻辑抽象出来的。说句实在话在业务代码里直接写Iterator的人不多了大家都图省事用增强for。但理解Iterator仍然非常重要因为它是理解所有Java集合遍历的基石也是面试官最爱深挖的一个点。你只要弄懂了Iterator的工作机制后面什么fail-fast、什么ConcurrentModificationException、什么CopyOnWriteArrayList的弱一致性迭代器都能顺着这条线串起来。2. 更现代的遍历方式——函数式风格与迭代晋级2.1 Collection.forEach方法与Iterable.forEach的关系从JDK 8开始Iterable接口上新增了一个默认方法forEach它接收一个Consumer函数式接口作为参数。于是集合遍历可以写成这样ListString list Arrays.asList(java, 遍历, 方法); list.forEach(item - System.out.println(item)); // 也可以写成方法引用 list.forEach(System.out::println);这里有一个重要的细节值得强调List接口重写了forEach的默认实现其他集合类用的才是Iterable接口里的默认实现。List的默认重写版本会把元素逐个用ListIterator来访问之所以这么做是为了让forEach和ListIterator的“弱一致性”行为保持一致同时对ArrayList这类结构来说性能也会更优。如果你在阅读源码时留意到ArrayList或LinkedList里确实有一个专门重写的forEach方法而不是直接继承Iterable的默认实现恰恰就是这个原因。forEach的写法比增强for更简洁而且可以链式调用、配合方法引用让代码非常优雅。但它有两个明显的限制一是同样拿不到索引二是lambda表达式里被引用的局部变量必须是“实际上不可变”的effectively final这给累加、计数之类的操作增加了不少麻烦。想要在遍历过程中做累加操作你得借助一个AtomicInteger之类的对象来绕开这个限制在提倡简洁的Java 8代码里很多人第一反应会去用Stream这一点下面再讲。2.2 Stream流式操作遍历只是函数式流水线的入口Stream是Java 8引入的又一大杀器。它本身并不是一种数据结构而是基于数据源集合、数组、文件行等生成的一个“元素序列”抽象层。调用集合的stream()方法之后你可以通过filter、map、sorted等一系列中间操作和终结操作来写出一整条数据处理流水线。遍历相关的典型代码如下ListString list Arrays.asList(java, 遍历, 方法, spring); list.stream() .filter(item - item.length() 2) .map(String::toUpperCase) .forEach(System.out::println);这里的forEach既是Stream的终结操作也就是“遍历输出”这一步。和Collection.forEach相比Stream.forEach在并行处理上更值得关注当你调用parallelStream()时Stream会采用ForkJoinPool把集合划分成多个子任务并行处理。如果集合规模巨大、元素处理本身也比较耗时并行流可能带来几倍的性能提升但这里的关键警告是——并行流默认使用公共线程池不要在里面做重量级的外部IO操作否则会拖垮整个JVM进程的所有并行任务。我在实际项目中就吃过这个亏一个定时任务里的并行流去查数据库结果把同时运行的其他定时任务全堵住了定位了整整一下午。另外Stream还有一点新手很容易踩坑Stream是单向且只能消费一次的不能重复遍历。如果你调用了list.stream().filter(...).forEach(...)之后还想着再用同一条流继续操作编译期就会给你报错因为流不能被复用。这种限制看似不友好但它天然引导开发者把逻辑写成“一次流水线完整跑完”的形式思路更清晰。2.3 ListIterator双向迭代的隐藏能力在上述基础之外List接口特有的ListIterator也是一个值得单独说明的迭代器。它可以向前遍历也可以向后遍历还能够在遍历过程中替换元素或插入元素。它额外提供了hasPrevious()、previous()、add(E e)、set(E e)等方法。来看一段典型用法ListString list new ArrayList(Arrays.asList(java, 遍历, 方法)); ListIteratorString lit list.listIterator(list.size()); // 从尾部向头部遍历 while (lit.hasPrevious()) { String item lit.previous(); System.out.println(item); }这种双向迭代在某些场景里非常有用比如你有一个按时间排序的列表可能要倒着遍历查找某个日期之前的记录或者在遍历过程中既要判断前一个元素又要判断后一个元素。ListIterator都能在一个迭代器对象内完成不需要额外维护索引和状态。我用过的另一个实用场景是在遍历一个有序列表的同时根据逻辑动态地在当前位置插入新元素。ListIterator的add操作会把新元素放入迭代器当前指向的位置之前并且保证迭代器后续的遍历能够感应到插入的元素这是普通增强for完全做不到的。不过因为ListIterator接口方法比较多多数业务代码里使用的频率并不高属于“需要的时候很关键、平时想不起来”的类型。到这里你可以做一个小小的对比总结普通for、增强for、Iterator、forEach、Stream.forEach、ListIterator这六种遍历方式分别适合不同的场景。我在后面的表格里会统统汇总起来但在此之前还有一个绕不开的重量级话题需要先解决掉——遍历时删除元素这个每个Java开发者迟早都会碰到的难题。3. 遍历中最容易翻车的操作——删除元素与并发修改3.1 ConcurrentModificationException的触发机制很多Java新手第一次遇到ConcurrentModificationException都是在“一边遍历一边删除元素”的时候。报错信息提示“modCount ! expectedModCount”但很多人看了一头雾水不知道这两个变量到底是什么关系。这里要把底层机制讲清楚。在ArrayList的内部有一个modCount字段它记录的是这个列表被结构性修改的次数。所谓结构性修改就是改变列表元素数量大小的操作比如add、remove、clear等。当创建Iterator的时候迭代器内部会保存一份当时的modCount作为expectedModCount。之后每次调用next()迭代器都会检查当前列表的modCount和expectedModCount是否一致不一致就抛出ConcurrentModificationException。所以经典的错误代码如下ListString list new ArrayList(Arrays.asList(a, b, c, d, e)); for (String item : list) { if (c.equals(item)) { list.remove(item); // 抛出 ConcurrentModificationException } }表面上看起来只是简单地判断并删除一个元素跑到第二次循环的时候next()内部一检查modCount发现已经被改过了直接抛异常。这里的关键在于ArrayList的迭代器并不知道外部发生了修改它默认认为集合结构不应该在你遍历时被随意改动。3.2 几种安全删除集合元素的方式比较既然直接删除会翻车那正确的做法是什么呢下面列出几种安全删除的方式并且说清楚各自的代价第一种使用Iterator的remove方法。IteratorString it list.iterator(); while (it.hasNext()) { String item it.next(); if (c.equals(item)) { it.remove(); } }因为Iterator的remove内部会同步更新expectedModCount所以不会触发并发修改异常。它也是唯一一种在“遍历过程中”真正安全删当前元素的做法。注意remove之前必须调用过next()否则会抛出IllegalStateException因为迭代器根本不知道要删哪个元素。第二种普通for循环倒序删除。for (int i list.size() - 1; i 0; i--) { if (c.equals(list.get(i))) { list.remove(i); } }利用倒序遍历删除后面的元素不会影响前面元素的下标位置。这种方式完全没有用到迭代器所以不会触发并发修改异常。缺点是只能用于支持随机访问的ListLinkedList用get(i)会性能退化。第三种先收集要删的元素循环结束后统一删除。ListString toRemove new ArrayList(); for (String item : list) { if (item.startsWith(c)) { toRemove.add(item); } } list.removeAll(toRemove);这个思路本质上是“遍历归遍历、删除归删除”把两者彻底分开。它思路单纯、容易理解但会额外开辟一个列表来存放待删除元素对超大集合来说存在空间开销。第四种使用removeIf方法最简洁的写法。list.removeIf(item - item.startsWith(c));removeIf是JDK 8在Collection接口上新增的默认方法内部帮你封装了迭代器遍历和remove的完整逻辑实现干净且高效。我在实际项目中推荐优先使用这种方式代码最少、意图最明确。如果你还在用Java 7及以下版本才需要考虑手动用Iterator方式处理。这里顺带说一个很实用的经验如果遍历过程中需要“同时”做删除以外的其他判断比如既要删除条件A的项又要收集条件B的项那最好不要混合多种遍历改写方式容易把代码搞混乱。我的通常做法是先遍历一轮收集结果信息再统一调用removeIf处理删除整个流程清晰且可测试。3.3 fail-fast与fail-safe两种并发安全策略前面提到的ConcurrentModificationException机制就是fail-fast策略它的设计哲学是“一旦检测到并发修改立刻失败”。这种策略的好处是缺陷暴露得早、代码不容易藏雷但缺陷是它只适用于单线程环境下的误修改检测并不能解决多线程并发修改集合的生产问题——你就算在遍历时捕获了这个异常数据也可能已经被破坏了一半。与之对应的还有一种fail-safe策略典型代表是CopyOnWriteArrayList和ConcurrentHashMap。它们维护的迭代器具有弱一致性迭代器遍历的是集合的一个“快照”或者当前已有的数据创建迭代器之后对集合的修改并不会影响迭代器的遍历行为也不会抛异常。CopyOnWriteArrayList的底层实现是“修改时复制整个底层数组”所以并发读性能好、写性能差ConcurrentHashMap则通过分段锁或者CAS机制来实现高并发下的读写。实际项目中如果集合本身会并发修改同时又需要安全遍历我的建议是优先考虑并发容器而不是普通容器加synchronized块。因为synchronized会把整个遍历操作锁死并发性能损失太严重而CopyOnWriteArrayList、ConcurrentHashMap这类专门设计的容器在多数读写场景下的表现要平滑得多。4. 不同数据结构的遍历差异——List、Set、Map与树形结构4.1 List的遍历细节ArrayList与LinkedList的差距List接口下最常用的实现类是ArrayList和LinkedList。前面已经粗略提到过两者的底层差异这里把遍历相关的结论归纳得更系统一点ArrayList适合普通for循环随机访问因为get(i)是O(1)。它同样适配增强for、Iterator、forEach和Stream整体效率都很优秀。LinkedList适合用迭代器或者增强for做顺序遍历因为每次next()只是沿着链表的next指针走一步整体复杂度为O(n)。但如果在LinkedList上用普通forget(i)来遍历每次get都要从链表头重新找起整体退化为O(n²)。可以做一个十分钟就能完成的压测验证构造一个包含10万个元素的LinkedList用普通for遍历打印每个元素再换用增强for遍历。实测下来普通for的耗时可能是增强for的几十倍甚至上百倍数据越明显。面试或者技术分享的时候这个测试效果非常直观我已经在好几次分享会上现场演示过了。还有一个容易被忽略的点是List.subList返回的子列表视图。如果你在subList遍历过程中修改了原list的结构也会导致并发修改问题因为subList本质上持有原list的引用并且共享modCount校验机制。日常写代码时如果需要对子列表进行遍历和删除操作最好先复制出一个新的ArrayList比如new ArrayList(list.subList(from, to))免得摸不着头脑地报异常。4.2 Map遍历四大主流方式Map并不直接继承Collection所以不能用集合那套iteratorMap本身没有实现Iterator接口但Map提供了多种遍历入口。这里以最常见的HashMap为例演示四种主流方式方式一通过keySet遍历键再通过键获取值。MapString, Integer map new HashMap(); for (String key : map.keySet()) { Integer value map.get(key); System.out.println(key - value); }这种方式胜在逻辑直接但问题在于每取一个value都要用key去哈希表里查询一次HashMap的get操作虽然是O(1)但常数开销依然存在。如果循环次数很大这种方式的总耗时一般比直接遍历entrySet要高出一截。方式二通过entrySet遍历键值对。for (Map.EntryString, Integer entry : map.entrySet()) { String key entry.getKey(); Integer value entry.getValue(); System.out.println(key - value); }这是我个人在业务代码中最常用的一种方式——一次性取出entry然后从entry里分别取key和value少了一次哈希查找综合性能最好。从遍历顺序上看HashMap的迭代顺序其实并不确定如果你需要按插入顺序遍历应该使用LinkedHashMap如果需要按自然排序遍历考虑TreeMap。方式三使用Map.forEach方法。map.forEach((key, value) - System.out.println(key - value));这是Java 8引入的最简洁写法非常推荐在函数式风格为主的代码库里使用。lambda参数key和value分别对应entry的键和值代码量少了一大截可读性也好。它的底层内部就是遍历entrySet再做处理所以性能上和entrySet方式基本一致。方式四使用Stream流遍历。map.entrySet().stream() .filter(entry - entry.getValue() 10) .forEach(entry - System.out.println(entry.getKey()));这种方式适合在举出“需要过滤、映射、排序”的场景时使用它实际上是先把Map转化为entrySet的流再通过流式操作完成更复杂的数据处理。如果仅仅是遍历打印用Stream反而显得笨重这里要按需选择。关于HashMap还有一个比较新的变化从JDK 8开始HashMap在链表长度超过阈值默认8且数组容量大于等于64时会把链表转换成红黑树所以遍历时如果遇到某些桶里是红黑树结构你仍然能正常遍历但是此时entry的访问路径和纯链表是不同的需要重点评估的是“它同时保持了O(1)的get/put平均复杂度”而遍历整个HashMap整体上仍然需要访问所有桶和所有节点。想在实际项目里精准预判HashMap遍历的性能有时候没有想象中那么容易这是我和团队排查过不少次线上问题之后才形成的认识。4.3 Set、Queue与栈的遍历注意事项Set集合没有get(index)这种随机访问方法所以它压根不能使用普通for循环按下标遍历。HashSet的遍历方式就是增强for、Iterator、forEach、Stream这些。这里有一点值得注意HashSet的迭代顺序受哈希值影响对调用者来说完全不可控如果业务上对顺序敏感就换LinkedHashSet或者TreeSet。Queue往往被人忽略其实Queue也继承了Collection所以可以用增强for或者Iterator去遍历但需要注意Queue的语义是FIFO遍历它并不等同于“消费队列”。如果你想一边遍历一边弹出元素应该用poll()方法循环取出而不是顺序遍历。Deque双端队列的实现类ArrayDeque、LinkedList也可以用迭代器正向或者反向遍历其中ArrayDeque禁止存入null元素遍历判断时要注意这个问题。栈结构在Java里通常用Deque来模拟不建议使用历史遗留的Stack类它继承自Vector所有方法都加锁性能比较差。用Deque做栈时因为Deque同时继承了Collection和Queue接口你也可以使用Iterator、增强for等方式遍历栈但遍历的顺序通常是从栈底到栈顶的方向你需要弄清楚当前实现里迭代器到底是从哪一头开始的。我的建议是有遍历需求时优先显式地使用迭代器并且在代码注释里写清楚遍历方向不然后来维护的人很容易踩坑。4.4 树的遍历方法——递归、迭代与层序遍历树的遍历在面试中出现频率极高而且它的“遍历方法及优缺点”和数组、列表的遍历有本质不同树结构既不是线性下标结构也不完全是类似Map那样的键值结构。它天然适用递归和显式栈、队列来做深度优先和广度优先遍历。二叉树的深度优先遍历可以细分为前序、中序、后序三种。最简单的实现是递归void preOrder(TreeNode node) { if (node null) return; System.out.println(node.val); preOrder(node.left); preOrder(node.right); }递归的优点是代码直觉极强、逻辑非常清晰缺点则是每层递归都会占用方法栈如果树很深比如极端退化成一条链表递归深度可能达到几万层最终导致StackOverflowError。因此面对有可能退化得很深的树结构应优先考虑使用显式的栈来模拟递归过程。用栈模拟前序遍历的典型写法是DequeTreeNode stack new ArrayDeque(); stack.push(root); while (!stack.isEmpty()) { TreeNode node stack.pop(); if (node null) continue; System.out.println(node.val); // 注意先压右子树再压左子树这样才能保证左子树先被弹出 stack.push(node.right); stack.push(node.left); }这种方式把递归调用栈换成了堆上的显式数据结构避免了栈溢出风险也更容易控制遍历节奏只是写出来比递归要多几行代码。层序遍历也叫广度优先遍历是另一个高频考点它使用队列来实现QueueTreeNode queue new LinkedList(); queue.offer(root); while (!queue.isEmpty()) { int levelSize queue.size(); // 关键点记录当前层的节点数量 for (int i 0; i levelSize; i) { TreeNode node queue.poll(); System.out.println(node.val); if (node.left ! null) queue.offer(node.left); if (node.right ! null) queue.offer(node.right); } }这里的levelSize是整个层序遍历最核心的技巧它保证了你可以在遍历过程中区分出“每一层”的节点边界。这个技巧在实际应用中非常常用常见的最右节点、每层最大值等同类问题几乎都要用到这个思路。相比深度优先遍历层序遍历的优点是天然逐层处理适合解决最短路径、按层统计、按层输出类型的问题缺点则是空间复杂度可能较高如果树的节点分布呈完全二叉树的形式队列里同时存在的节点数可能接近n/2。5. 遍历性能、最佳实践与常见的坑位速查5.1 性能对比简评什么场景选哪个网上关于不同遍历方式的性能测试很多但基于我的实战经验先把结论稳定下来在ArrayList这种支持随机访问的List结构中普通for和增强for的性能几乎没差别Iterator略慢一点点但也可以忽略不计forEach在ArrayList上因为内部重写过实现表现已经很接近增强forStream.forEach因为多了流水线抽象的开销在大数据量下一般会慢于传统方式但差距通常不到一倍代码清晰度带来的收益完全能覆盖这点性能损失。如果是LinkedList这种顺序结构情况会反转普通forget(i)方式千万不能用增强for和Iterator由于只用next指针移动性能反而接近ArrayList的水平。至于InputStream、数据库查询返回的游标这类“延迟数据源”则不能直接套用集合遍历的思维需要特殊处理因为集合遍历要求数据先完整加载进内存。我把核心的遍历方式适用场景整理成一个速查表方便你收藏后随手参考遍历方式适用于能否删除元素能否获取索引备注普通forArrayList、数组可以注意下标变化能LinkedList上性能极差增强for所有Collection、数组循环体内不行不能底层是Iterator或下标Iterator所有Collection能用it.remove()不能最安全的边遍历边删方式forEachList重写过实现、其他Collection循环体内不行不能简洁适合元素处理型Stream.forEach所有Collection配合Stream API使用不行不能支持并行流适合流水线ListIterator仅List能add/set/remove能取nextIndex等双向前进功能最全5.2 数组遍历的一个常见争执到底哪种好数组在Java里的遍历看起来和List很相似但有一个区别值得单独写一行数组是定长的没有Iterator增强for遍历数组时编译器生成的是普通下标循环的字节码。因此在数组遍历场景中普通for和增强for本质上没有区别纯粹取决于你需不需要显式使用下标。数组还有一个List没有的操作方式通过Arrays.stream方法转换为IntStream、LongStream或DoubleStream再遍历。这种写法和Stream链式操作无缝衔接很适合数值统计类的场景。另一个小技巧是使用Objects.equals配合Arrays.deepEquals来做数组内容比较这不属于遍历范畴但常常和遍历一起出现在实际编码需求里算是我顺带分享的一个小经验。5.3 遍历中的常见坑位速查表为了方便以后排查问题我把遍历过程中最容易踩的坑整理成了一个速查表问题原因解决方案ConcurrentModificationException遍历时通过集合方法修改了结构使用Iterator.remove、removeIf、倒序删除LinkedList上用普通for遍历巨慢每次get都是O(n)扫描换增强for或IteratorHashMap遍历顺序和预期不一致HashMap不保证顺序按需改用LinkedHashMap或TreeMapLambda表达式里修改外部变量报错变量必须是effectively final改用AtomicInteger或者包装数组Stream被二次使用时报错Stream只能消费一次每次需要使用时重新创建StreamCopyOnWriteArrayList并发修改不报错但拿不到最新值迭代器基于快照需要最新值时使用get(index)再判断树递归遍历栈溢出递归深度过大改用显式栈或层序遍历subList视图修改原List报异常共享modCount校验复制成一个新ArrayList再操作遍历中调用list.size()并动态添加元素无限循环或者漏处理明确固定遍历边界或用普通for条件控制这些坑我在面试候选人的时候见过太多次了。其实如果你能主动讲出其中两三个坑的底层原因和解决方案已经能证明自己的工程经验不是靠背题来的。5.4 关于遍历的工程习惯建议最后分享几个我在团队代码规范和Code Review中反复强调的工程习惯第一优先选择“能表达意图”的遍历方式。如果你只是要挨个处理元素用增强for或者forEach不要用Iterator绕来绕去。如果你要从集合中筛选、转换后再处理直接用Stream流水线别在循环里堆业务逻辑和临时变量。第二遍历时务必想清楚“删除”这件事。无论你的意图是删除当前元素、删除部分元素还是清空整个集合都有对应的高效写法不要一律套用for循环加remove的暴力组合。多使用removeIf这些语义清晰的方法代码会好维护得多。第三注意遍历顺序是否重要。HashMap不保证顺序HashSet也依赖哈希值无序遇到对顺序有要求的业务逻辑从一开始就要选择正确的数据结构。顺序问题不是遍历写法能解决的必须在数据结构的选型阶段想清楚。第四关注大集合遍历时的GC压力。像Stream并行流、自动装箱、临时对象创建在大数据量下都会产生显著的GC开销。遍历本身通常不慢慢的是每次循环里创建的无用对象。这一条我压箱底的经验就是能复用对象就不新建能用基本类型数组就别用包装类集合用IntStream处理数字集合就是一个非常典型的优化方式。结尾一年后的又一次填坑写到这里突然想起去年排查过的一个线上接口慢问题。当时服务里有一段代码用普通for循环遍历一个LinkedList数据量不过几十万单次请求响应时间却飙到了十几秒。换掉数据结构或者遍历方式之后响应时间直接降到几十毫秒这个对比我一直记忆犹新。遍历在Java里看起来是最基础的操作但恰恰是这种“太基础”的写法最容易让人麻痹大意。很多人直到生产环境出了问题才意识到自己写下的每一行代码背后的数据结构、迭代机制、并发策略都在暗中决定系统的性能和行为。理解遍历的各种方式以及它们的优缺点不是一件可以投机取巧的事它会在你排查问题、设计系统、做技术面试的每一个环节持续发挥作用。至少对我来说这是整个Java集合框架中最值得花时间吃透的一块知识。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →