章
目
录
这篇Java教程讨论了不同遍历HashMap的方式,并比较了每种方式的性能,以便我们知道HashMap遍历效率最高的方法是哪种。
1.引言
在这篇文章中,我决定比较不同遍历HashMap的方式的性能。HashMap是一个广泛使用的类,大多数情况下,我们使用该类提供的get(Object key)方法来获取值。但有时需要遍历整个Map并获取其中存储的所有键-值对。
例如,分析从客户端发送的所有请求参数。在这种情况下,对于每个客户端,我们至少在请求处理期间遍历整个映射。
如果我们在代码中的许多地方都使用这种类型的迭代,而且有许多请求,那么我们肯定希望优化HashMap迭代代码,以充分利用它。我下面的分析将帮助我们决定下一步该做什么。
2.遍历Map的不同方式
让我们从不同遍历HashMap的方式开始。HashMap定义如下:
Map<String, Integer> map = new HashMap();
2.1. HashMap.forEach()
自Java 8以来,我们可以使用forEach()方法遍历Map中存储的键和值。
map.forEach((key, value) -> {
System.out.println(key + ": " + value);
//...
});
2.2. 遍历Map的条目
entrySet()方法返回一个Set视图,其中包含所有的条目,我们可以像普通的HashSet一样遍历它。
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
//...
}
2.3. 遍历Map的键并访问值
keySet()方法返回所有键的集合。我们可以遍历所有键,并使用它们来访问Map中的值。请注意,这需要额外的步骤来访问值,所以在大多数情况下,这不是推荐的方法。
for (String key : map.keySet()) {
Integer value = map.get(key);
//...
}
2.4. 使用迭代器
由于我们从entrySet()和keySet()获取Set视图,因此我们可以使用迭代器遍历Map。
以下代码使用迭代器遍历Map条目。
Iterator<Entry<String, Integer>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Integer> entry = entryIterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
//...
}
同样,以下代码使用迭代器遍历Map的键,并使用键来访问值。
Iterator<String> keySetIterator = map.keySet().iterator();
while (keySetIterator.hasNext()) {
String key = keySetIterator.next();
Integer value = map.get(key);
//...
}
3.比较不同技术的性能
现在让我们比较对Map中存储的常见数据集执行所有类型的遍历的性能。我们在Map中存储了100万个键-值对。
Map<String, Integer> map = new HashMap();
for (int i = 0; i < 10_00_000; i++) {
map.put(String.valueOf(i), i);
}
我们将以四种方式遍历Map。我们还将以最合适的方式从Map中获取所有条目的键和值。我们使用JMH模块来对所有方法的性能进行基准测试。
@Benchmark
public void usingForEach(Blackhole blackhole) {
map.forEach((key, value) -> {
blackhole.consume(key);
blackhole.consume(value);
});
}
@Benchmark
public void usingEntrySetWithForLoop(Blackhole blackhole) {
for (Map.Entry<String, Integer> entry : map.entrySet()) {
blackhole.consume(entry.getKey());
blackhole.consume(entry.getValue());
}
}
@Benchmark
public void usingKeySetWithForLoop(Blackhole blackhole) {
for (String key : map.keySet()) {
blackhole.consume(map.get(key));
}
}
@Benchmark
public void usingEntrySetWithForIterator(Blackhole blackhole) {
Iterator<Entry<String, Integer>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Integer> entry = entryIterator.next();
blackhole.consume(entry.getKey());
blackhole.consume(entry.getValue());
}
}
4.结果
以上程序的基准测试得分是:
c.h.c.collections.map...usingForEach thrpt 15 89.044 ± 2.703 ops/s
c.h.c.collections.map...usingEntrySetWithForLoop thrpt 15 54.906 ± 6.326 ops/s
c.h.c.collections.map...usingKeySetWithForLoop thrpt 15 52.163 ± 4.517 ops/s
c.h.c.collections.map...usingEntrySetWithForIterator thrpt 15 63.494 ± 4.334 ops/s
虽然所有方法之间的差异不大,但我们可以看到:
- 使用for循环与keySet()、entrySet()方法几乎表现相同。
- 使用iterator()方法性能较差。
- 令人惊讶的是,forEach()方法需要最长的时间。这是因为forEach方法使用内部迭代,这意味着迭代逻辑在HashMap实现内部处理。它为HashMap中的每个元素调用提供的lambda表达式。这种内部迭代会带来一些开销,因为涉及到lambda表达式的执行和额外的函数调用。
出于代码的清晰性,我们可以得出以下结论:
- 如果Map仅包含少量条目,则使用HashMap.forEach()是遍历HashMap的最佳方式。
- 如果Map包含100万个条目,那么使用HashMap.entrySet()是遍历HashMap的最佳方式。
5.结论
对于大多数应用需求来说,100万是一个非常大的数字。尽管与毫秒级的差异相比,并不是非常显著,但与for循环的情况相比,差异很大。我认为大多数人可以容忍这种微小的差异。
但如果你想得出结论,使用entry set非常明确,并且比其他方法性能更好。当多次执行上述程序时,结果的变化在20%到50%之间。
扩展阅读:Java中多种循环性能的区别比较