为什么你的代码会无缘无故变慢?
\n想象一下:你写了一个完美的算法,通过了所有单元测试,但在生产环境中,应用程序突然开始“卡顿”。大多数情况下,问题不在于算法本身,而在于你遍历集合的方式。这一点尤其适用于Java Map——这种数据结构出现在10个企业项目中的9个里。
\n在Stack Overflow上,“How do I efficiently iterate over each entry in a Java Map?”这个问题获得了数万次浏览,这并非偶然。许多开发者,即使有2-3年的经验,也在使用非最优的方法。让我们来弄清楚如何正确地遍历Map,以及为什么一种方法可能比另一种方法快10倍。
\n\nJava Map的内部结构:它里面有什么?
\n在讨论迭代之前,我们先回顾一下它的结构。HashMap是一个带有桶的哈希表。TreeMap是一棵红黑树。LinkedHashMap是一个带有双向链表的混合体。这些结构都存储键值对,但它们的遍历方式在速度和内存消耗上有所不同。
Map提供的主要视图集合(view collections)如下:
\n- \n
keySet()— 键的集合 \n values()— 值的集合 \n entrySet()— 键值对的集合 \n
正是通过这三个“窗口”,我们来查看数据。
\n\n方法1:通过for-each使用经典的entrySet(最流行)
\n这是一种基础但非常高效的模式。你可以在一次迭代中同时访问键和值。
\nMap<String, Integer> map = new HashMap<>();\n// ... 填充map\nfor (Map.Entry<String, Integer> entry : map.entrySet()) {\n String key = entry.getKey();\n Integer value = entry.getValue();\n // 你的逻辑\n}\n为什么这很高效? 因为entrySet()返回的是一个视图,而不是数据的副本。迭代器遍历HashMap的内部节点(Node),每次调用next()都会立即同时提供键和值。没有额外的哈希查找。
何时使用: 当你需要键值对中的两个组件时。这是Java社区的事实标准。
\n\n方法2:keySet + get(应避免的反模式)
\n许多新手会这样写:
\nfor (String key : map.keySet()) {\n Integer value = map.get(key);\n // 逻辑\n}\n乍一看,代码可读性不错。但在内部,HashMap.get()每次都会重新计算键的哈希值并遍历冲突链。对于100个元素来说,这几乎察觉不到,但对于100,000个元素来说,这会导致严重的性能下降。在最坏的情况下(大量冲突),复杂度会从O(n)变成O(n²)。
\n结论: 如果你需要键和值,请使用entrySet()。如果只需要键,那么keySet()是理想的选择。
方法3:使用迭代器进行显式删除(用于遍历时的修改操作)
\n有时需要在遍历Map的同时删除元素。for-each循环会抛出ConcurrentModificationException。这时迭代器就派上用场了:
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();\nwhile (iterator.hasNext()) {\n Map.Entry<String, Integer> entry = iterator.next();\n if (entry.getValue() < 0) {\n iterator.remove(); // 安全删除\n }\n}\n这个方法在你需要“即时”过滤数据时特别有用。在HashMap中,迭代器的每次删除操作都是O(1)复杂度,因为它知道当前节点。
\n\n方法4:使用Lambda表达式的forEach(Java 8+)
\n从Java 8开始,Map有了自己的forEach方法:
map.forEach((key, value) -> {\n System.out.println(key + " -> " + value);\n});\n在底层