java HashMap 的 7 种遍历方式与性能分析
随着 JDK 1.8 Streams API 的发布,使得 HashMap 拥有了更多的遍历的方式,但应该选择那种遍历方式?反而成了一个问题。
本文先从 HashMap 的遍历方法讲起,然后再从性能、原理以及安全性等方面,来分析 HashMap 各种遍历方式的优势与不足,本文主要内容如下图所示:

一、HashMap 遍历
HashMap 遍历从大的方向来说,可分为以下 4 类:
- 迭代器(Iterator)方式遍历;
- For Each 方式遍历;
- Lambda 表达式遍历(JDK 1.8+);
- Streams API 遍历(JDK 1.8+)。
但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:
- 使用迭代器(Iterator)EntrySet 的方式进行遍历;
- 使用迭代器(Iterator)KeySet 的方式进行遍历;
- 使用 For Each EntrySet 的方式进行遍历;
- 使用 For Each KeySet 的方式进行遍历;
- 使用 Lambda 表达式的方式进行遍历;
- 使用 Streams API 单线程的方式进行遍历;
- 使用 Streams API 多线程的方式进行遍历。
接下来我们来看每种遍历方式的具体实现代码。
import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class HashMapTest { public static void main(String[] args) { // 创建并赋值 HashMap Map<Integer, String> map = new HashMap(); map.put(1, "Java"); map.put(2, "JDK"); map.put(3, "Spring Framework"); map.put(4, "MyBatis framework"); map.put(5, "Java中文社群"); // 遍历 // 1 迭代器 EntrySet System.out.println(" ********** 迭代器 EntrySet ********** "); Iterator<Map.Entry<Integer, String>> iterator1 = map.entrySet().iterator(); while (iterator1.hasNext()) { Map.Entry<Integer, String> entry = iterator1.next(); System.out.println(entry.getKey() + "\t" + entry.getValue()); // System.out.println(entry.getValue()); } // 2 迭代器 KeySet System.out.println(" ********** 迭代器 KeySet ********** "); Iterator<Integer> iterator2 = map.keySet().iterator(); while (iterator2.hasNext()) { Integer key = iterator2.next(); System.out.println(key + "\t" + map.get(key)); // System.out.println(map.get(key)); } // 3 ForEach EntrySet System.out.println(" ********** ForEach EntrySet ********** "); for (Map.Entry<Integer, String> entry : map.entrySet()) { System.out.println(entry.getKey() + "\t" + entry.getValue()); //System.out.println(entry.getValue()); } // 4 ForEach KeySet System.out.println(" ********** ForEach KeySet ********** "); for (Integer key : map.keySet()) { System.out.println(key + "\t" + map.get(key)); //System.out.println(map.get(key)); } // 5 Lambda System.out.println(" ********** Lambda ********** "); map.forEach((key, value) -> { System.out.println(key + "\t" + value); //System.out.println(value); }); // 6 Streams API 单线程 System.out.println(" ********** Streams API 单线程 ********** "); map.entrySet().stream().forEach((entry) -> { System.out.println(entry.getKey() + "\t" + entry.getValue()); //System.out.println(entry.getValue()); }); // 7 Streams API 多线程 System.out.println(" ********** Streams API 多线程 ********** "); map.entrySet().parallelStream().forEach((entry) -> { System.out.println(entry.getKey() + "\t" + entry.getValue()); //System.out.println(entry.getValue()); }); } }
以上程序的执行结果为:
********** 迭代器 EntrySet ********** 1 Java 2 JDK 3 Spring Framework 4 MyBatis framework 5 Java中文社群 ********** 迭代器 KeySet ********** 1 Java 2 JDK 3 Spring Framework 4 MyBatis framework 5 Java中文社群 ********** ForEach EntrySet ********** 1 Java 2 JDK 3 Spring Framework 4 MyBatis framework 5 Java中文社群 ********** ForEach KeySet ********** 1 Java 2 JDK 3 Spring Framework 4 MyBatis framework 5 Java中文社群 ********** Lambda ********** 1 Java 2 JDK 3 Spring Framework 4 MyBatis framework 5 Java中文社群 ********** Streams API 单线程 ********** 1 Java 2 JDK 3 Spring Framework 4 MyBatis framework 5 Java中文社群 ********** Streams API 多线程 ********** 4 MyBatis framework 1 Java 5 Java中文社群 2 JDK 3 Spring Framework
二、性能测试&字节码分析
entrySet 的性能比 keySet 的性能高出了一倍之多,因此我们应该尽量使用 entrySet 来实现 Map 集合的遍历。
从结果可以看出,除了 Lambda 和 Streams API 之外,通过迭代器循环和 for 循环的遍历的 EntrySet 最终生成的代码是一样的,他们都是在循环中创建了一个遍历对象 Entry;而 KeySet 的代码也是类似的;
所以在使用迭代器或是 for 循环 EntrySet 时,性能都是相同的,因为它们最终生成的字节码基本都是一样的;同理 KeySet 的两种遍历方式也是类似的。
测试详见原文:https://mp.weixin.qq.com/s/zQBN3UvJDhRTKP6SzcZFKw
三、性能测试&字节码分析
EntrySet 之所以比 KeySet 的性能高是因为,KeySet 在循环时使用了 map.get(key),而 map.get(key) 相当于又遍历了一遍 Map 集合去查询 key 所对应的值。为什么要用“又”这个词?那是因为在使用迭代器或者 for 循环时,其实已经遍历了一遍 Map 集合了,因此再使用 map.get(key) 查询时,相当于遍历了两遍。
而 EntrySet 只遍历了一遍 Map 集合,之后通过代码“Entry<Integer, String> entry = iterator.next()”把对象的 key 和 value 值都放入到了 Entry 对象中,因此再获取 key 和 value 值时就无需再遍历 Map 集合,只需要从 Entry 对象中取值就可以了。
所以,EntrySet 的性能比 KeySet 的性能高出了一倍,因为 KeySet 相当于循环了两遍 Map 集合,而 EntrySet 只循环了一遍。
四、安全性问题
我们不能在遍历中使用集合 map.remove() 来删除数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的删除集合的方式。同样的我们也可以使用 Lambda 中的 removeIf 来提前删除数据,或者是使用 Stream 中的 filter 过滤掉要删除的数据进行循环,这样都是安全的,当然我们也可以在 for 循环前删除数据在遍历也是线程安全的。
五、总结
本文讲了 HashMap 4 种遍历方式:迭代器、for、lambda、stream,以及具体的 7 种遍历方法,综合性能和安全性来看,应该尽量使用迭代器(Iterator)来遍历 EntrySet 的遍历方式来操作 Map 集合,这样就会既安全又高效了。
原文出处:https://mp.weixin.qq.com/s/zQBN3UvJDhRTKP6SzcZFKw

浙公网安备 33010602011771号