Java:通过 HashMap 进行迭代,哪个更高效?
Posted
技术标签:
【中文标题】Java:通过 HashMap 进行迭代,哪个更高效?【英文标题】:Java : Iteration through a HashMap, which is more efficient? 【发布时间】:2011-08-15 03:29:26 【问题描述】:鉴于以下代码,有两种替代方法可以迭代它,这两种方法之间有什么性能差异吗?
Map<String, Integer> map = new HashMap<String, Integer>();
//populate map
//alt. #1
for (String key : map.keySet())
Integer value = map.get(key);
//use key and value
//alt. #2
for (Map.Entry<String, Integer> entry : map.entrySet())
String key = entry.getKey();
Integer value = entry.getValue();
//use key and value
我倾向于认为alt. #2
是遍历整个map
的更有效方法(但我可能错了)
【问题讨论】:
地图到底有多大?这听起来像是过早的优化。 @Matt 我问是因为我有几个,而且它们很大——通常是 10K-100K 元素;肯定有一个很好的优化案例! 更新:许多答案似乎认为这是过早的优化。请注意,上面确实是 SSCCE (sscce.org),而不是我要优化的实际代码! 为什么会出现这种过早优化?第二种选择显然更快,但也显然更自我记录。如果你想要的只是值,你还会使用键集迭代器吗?我希望不会。 我认为如果密钥很大和/或涉及非常多的 .equals ,这将成为一个更严重的问题 基准测试会很有趣:如何涉及 .equals 方法以便第二个选项至少慢两倍... 【参考方案1】:您的第二个选项肯定更有效,因为与第一个选项中的 n 次相比,您只进行一次查找。
但是,没有什么比尽可能尝试更好的了。所以这里 -
(不完美,但足以验证假设并在我的机器上)
public static void main(String args[])
Map<String, Integer> map = new HashMap<String, Integer>();
// populate map
int mapSize = 500000;
int strLength = 5;
for(int i=0;i<mapSize;i++)
map.put(RandomStringUtils.random(strLength), RandomUtils.nextInt());
long start = System.currentTimeMillis();
// alt. #1
for (String key : map.keySet())
Integer value = map.get(key);
// use key and value
System.out.println("Alt #1 took "+(System.currentTimeMillis()-start)+" ms");
start = System.currentTimeMillis();
// alt. #2
for (Map.Entry<String, Integer> entry : map.entrySet())
String key = entry.getKey();
Integer value = entry.getValue();
// use key and value
System.out.println("Alt #2 took "+(System.currentTimeMillis()-start)+" ms");
结果(一些有趣的)
与int mapSize = 5000; int strLength = 5;
Alt #1 耗时 26 毫秒
Alt #2 耗时 20 毫秒
与int mapSize = 50000; int strLength = 5;
Alt #1 耗时 32 毫秒
Alt #2 耗时 20 毫秒
与int mapSize = 50000; int strLength = 50;
Alt #1 耗时 22 毫秒
Alt #2 耗时 21 毫秒
与int mapSize = 50000; int strLength = 500;
Alt #1 耗时 28 毫秒
Alt #2 耗时 23 毫秒
与int mapSize = 500000; int strLength = 5;
Alt #1 耗时 92 毫秒
Alt #2 耗时 57 毫秒
...等等
【讨论】:
请谷歌了解如何进行有效的微基准测试。 (关键点:让热点在基准测试之前做一些预热。) @Paulo - 足够公平并指出。我重试了使用预热阶段(基本上运行整个序列一次,然后再次运行它进行测量),但结果非常一致。我想这是因为即使没有预热阶段,看跌期权也在预热? +1 @amol:感谢基准测试/确凿的证据@Paulo:您会推荐什么特定标准进行基准测试? 这已经很老了,但是当有人问到微基准测试时,有一个讨论它的线程:***.com/questions/504103/…【参考方案2】:第二个 sn-p 会稍微快一些,因为它不需要重新查找密钥。
所有HashMap
迭代器调用nextEntry
method,它返回一个Entry<K,V>
。
您的第一个 sn-p 丢弃条目中的值(在 KeyIterator
中),然后在字典中再次查找它。
您的第二个 sn-p 直接使用键和值(来自 EntryIterator
)
(keySet()
和 entrySet()
都是廉价电话)
【讨论】:
【参考方案3】:地图:
Map<String, Integer> map = new HashMap<String, Integer>();
除了这两个选项,还有一个。
1) keySet() - 如果您需要仅使用 keys
,请使用它for ( String k : map.keySet() )
...
2) entrySet() - 如果您需要两者,请使用它:keys & values
for ( Map.Entry<String, Integer> entry : map.entrySet() )
String k = entry.getKey();
Integer v = entry.getValue();
...
3) values() - 如果您需要仅 values
,请使用它for ( Integer v : map.values() )
...
【讨论】:
【参考方案4】:后者比前者更有效率。 FindBugs 之类的工具实际上会标记前者并建议您使用后者。
【讨论】:
+1 @Jonas:感谢您提到 FindBugs - 每天学习新东西!【参考方案5】:最有效的方法(根据我的基准测试)是使用 Java 8 中添加的新 HashMap.forEach()
方法或 HashMap.entrySet().forEach()
。
JMH 基准测试:
@Param("50", "500", "5000", "50000", "500000")
int limit;
HashMap<String, Integer> m = new HashMap<>();
public Test()
@Setup(Level.Trial)
public void setup()
m = new HashMap<>(m);
for(int i = 0; i < limit; i++)
m.put(i + "", i);
int i;
@Benchmark
public int forEach(Blackhole b)
i = 0;
m.forEach((k, v) -> i += k.length() + v; );
return i;
@Benchmark
public int keys(Blackhole b)
i = 0;
for(String key : m.keySet()) i += key.length() + m.get(key);
return i;
@Benchmark
public int entries(Blackhole b)
i = 0;
for (Map.Entry<String, Integer> entry : m.entrySet()) i += entry.getKey().length() + entry.getValue();
return i;
@Benchmark
public int keysForEach(Blackhole b)
i = 0;
m.keySet().forEach(key -> i += key.length() + m.get(key); );
return i;
@Benchmark
public int entriesForEach(Blackhole b)
i = 0;
m.entrySet().forEach(entry -> i += entry.getKey().length() + entry.getValue(); );
return i;
public static void main(String[] args) throws RunnerException
Options opt = new OptionsBuilder()
.include(Test.class.getSimpleName())
.forks(1)
.warmupIterations(25)
.measurementIterations(25)
.measurementTime(TimeValue.milliseconds(1000))
.warmupTime(TimeValue.milliseconds(1000))
.timeUnit(TimeUnit.MICROSECONDS)
.mode(Mode.AverageTime)
.build();
new Runner(opt).run();
结果:
Benchmark (limit) Mode Cnt Score Error Units
Test.entries 50 avgt 25 0.282 ± 0.037 us/op
Test.entries 500 avgt 25 2.792 ± 0.080 us/op
Test.entries 5000 avgt 25 29.986 ± 0.256 us/op
Test.entries 50000 avgt 25 1070.218 ± 5.230 us/op
Test.entries 500000 avgt 25 8625.096 ± 24.621 us/op
Test.entriesForEach 50 avgt 25 0.261 ± 0.008 us/op
Test.entriesForEach 500 avgt 25 2.891 ± 0.007 us/op
Test.entriesForEach 5000 avgt 25 31.667 ± 1.404 us/op
Test.entriesForEach 50000 avgt 25 664.416 ± 6.149 us/op
Test.entriesForEach 500000 avgt 25 5337.642 ± 91.186 us/op
Test.forEach 50 avgt 25 0.286 ± 0.001 us/op
Test.forEach 500 avgt 25 2.847 ± 0.009 us/op
Test.forEach 5000 avgt 25 30.923 ± 0.140 us/op
Test.forEach 50000 avgt 25 670.322 ± 7.532 us/op
Test.forEach 500000 avgt 25 5450.093 ± 62.384 us/op
Test.keys 50 avgt 25 0.453 ± 0.003 us/op
Test.keys 500 avgt 25 5.045 ± 0.060 us/op
Test.keys 5000 avgt 25 58.485 ± 3.687 us/op
Test.keys 50000 avgt 25 1504.207 ± 87.955 us/op
Test.keys 500000 avgt 25 10452.425 ± 28.641 us/op
Test.keysForEach 50 avgt 25 0.567 ± 0.025 us/op
Test.keysForEach 500 avgt 25 5.743 ± 0.054 us/op
Test.keysForEach 5000 avgt 25 61.234 ± 0.171 us/op
Test.keysForEach 50000 avgt 25 1142.416 ± 3.494 us/op
Test.keysForEach 500000 avgt 25 8622.734 ± 40.842 us/op
如您所见,HashMap.forEach
和 HashMap.entrySet().forEach()
在大地图上表现最佳,并通过 entrySet()
上的 for 循环加入,以获得在小地图上的最佳性能。
keys 方法较慢的原因可能是因为它们必须为每个条目再次查找值,而其他方法只需要读取他们已经必须获取值的对象中的字段。我希望迭代器方法变慢的原因是它们正在进行外部迭代,这需要对每个元素进行两次方法调用(hasNext
和 next
),并将迭代状态存储在迭代器对象中,而forEach
完成的内部迭代只需要对accept
进行一次方法调用。
您应该使用目标数据在目标硬件上进行概要分析,并在循环中执行目标操作以获得更准确的结果。
【讨论】:
【参考方案6】:一般来说,对于 HashMap,第二个会快一点。只有当你有很多哈希冲突时才真正重要,因为那时get(key)
调用比O(1)
慢 - 它得到O(k)
,k
是同一个桶中的条目数(即具有相同哈希码或不同哈希码的键仍然映射到同一个存储桶 - 这也取决于映射的容量、大小和负载因子。
Entry-iterating 变体不必进行查找,因此它在这里变得更快。
另一个注意事项:如果您的地图的容量比实际大小大很多并且您经常使用迭代,您可以考虑使用 LinkedHashMap 代替。它为完整的迭代(以及可预测的迭代顺序)提供O(size)
而不是O(size+capacity)
复杂性。 (您仍然应该衡量这是否真的带来了改进,因为这些因素可能会有所不同。LinkedHashMap 创建地图的开销更大。)
【讨论】:
【参考方案7】:bguiz,
我认为(我不知道)迭代 EntrySet(备选方案 2)的效率略高,仅仅是因为它不会散列每个键以获得它的值...话虽如此,计算散列是每个条目的 O(1) 操作,因此我们只在整个 HashMap
...可能具有非常不同的性能特征。
我确实认为您会“推动”它以真正注意到性能差异。如果您担心,那么为什么不设置一个测试用例来为这两种迭代技术计时?
如果您没有报告过真实的性能问题,那么您真的担心的不是很多......这里和那里的几个时钟滴答声不会影响程序的整体可用性。
我相信代码的许多其他方面通常比直接性能更重要。当然,有些块是“性能关键的”,这在编写之前就已经知道了,更不用说性能测试了……但这种情况相当罕见。作为一种通用方法,最好专注于编写完整、正确、灵活、可测试、可重用、可读、可维护的代码……性能可以在以后根据需要构建。
版本 0 应该尽可能简单,没有任何“优化”。
【讨论】:
请注意,这绝对不是过早优化的情况,软件绝对不是零版本。它是现有的成熟软件,需要性能改进。在我的问题中,我发布的是 SSCCE (sscce.org)以上是关于Java:通过 HashMap 进行迭代,哪个更高效?的主要内容,如果未能解决你的问题,请参考以下文章
在 Ember 1.13 及更高版本中,当迭代字符串数组时,我应该使用哪个键?