给出以下代码,有两种替代方法来迭代它,
这两种方法之间是否有任何性能差异?
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
的更有效方法(但我可能错了)
答案 0 :(得分:58)
您的第二个选项肯定更有效率,因为您只进行一次查找,而在第一个选项中进行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 ms
Alt#2耗时20毫秒
使用int mapSize = 50000; int strLength = 50;
Alt#1耗时22毫秒
Alt#2耗时21毫秒
使用int mapSize = 50000; int strLength = 500;
Alt#1耗时28 ms
Alt#2耗时23毫秒
使用int mapSize = 500000; int strLength = 5;
Alt#1耗时92毫秒
Alt#2耗时57毫秒
......等等
答案 1 :(得分:10)
第二个片段会稍快一些,因为它不需要重新查找键。
所有HashMap
迭代器都会调用nextEntry
method,后者返回Entry<K,V>
。
您的第一个代码段会丢弃该条目中的值(在KeyIterator
中),然后在字典中再次查找。
您的第二个代码段直接使用密钥和值(来自EntryIterator
)
(keySet()
和entrySet()
都是廉价电话)
答案 2 :(得分:5)
后者比前者更有效率。像FindBugs这样的工具实际上会标记前者并建议你做后者。
答案 3 :(得分:5)
地图:
Map<String, Integer> map = new HashMap<String, Integer>();
除了2个选项外,还有一个选项。
1) keySet() - 如果您需要仅 键
,请使用它for ( String k : map.keySet() ) {
...
}
2) entrySet() - 如果您需要两者,请使用它: keys&amp;值强>
for ( Map.Entry<String, Integer> entry : map.entrySet() ) {
String k = entry.getKey();
Integer v = entry.getValue();
...
}
3) values() - 如果您需要 值
,请使用它for ( Integer v : map.values() ) {
...
}
答案 4 :(得分:2)
bguiz,
我认为(我不知道)迭代EntrySet(备选方案2)的效率稍微高一点,因为它不会散列每个键以获得它的值...说完了,计算哈希值每个条目是一个O(1)操作,因此我们只在整个HashMap
上讨论O(n)...但请注意,所有这些仅适用于HashMap
...其他实现Map
可能具有非常不同的性能特征。
我确实认为你会“推动它”实际上注意到性能上的差异。如果您担心,为什么不设置测试用例来计算两种迭代技术?
如果你没有真实的,报告的性能问题,那么你真的不用担心......这里有几个时钟滴答,这不会影响程序的整体可用性。
我相信代码的许多其他方面通常比直接性能更重要。当然,有些块是“性能至关重要的”,而且在它编写之前就已经知道了,单独进行了性能测试......但是这种情况相当罕见。作为一种通用方法,最好将注意力集中在编写完整,正确,灵活,可测试,可重用,可读,可维护的代码......性能可以在以后根据需要构建。
版本0应尽可能简单,没有任何“优化”。
答案 5 :(得分:2)
一般来说,第二个对于HashMap来说会快一点。如果你有很多哈希冲突,那真的很重要,因为get(key)
调用慢于O(1)
- 它得到O(k)
,其中k
是条目数同一个桶(即具有相同哈希码的密钥数或不同的哈希码仍然映射到同一个桶 - 这取决于地图的容量,大小和加载因子)。
Entry-iterating变体不必进行查找,因此它在这里变得更快。
另一个注意事项:如果地图的容量比实际大小大很多,并且您经常使用迭代,则可以考虑使用LinkedHashMap。它为完整迭代(以及可预测的迭代顺序)提供O(size)
而不是O(size+capacity)
复杂度。 (你仍然应该测量这是否真的有所改进,因为因素可能会有所不同.LinkedHashMap在创建地图时有更大的开销。)
答案 6 :(得分:1)
最有效的方法(根据我的基准)是使用Java 8或HashMap.entrySet().forEach()
中添加的新HashMap.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
的一个方法即可。
您应该使用目标数据在目标硬件上进行概要分析,并在循环中执行目标操作,以获得更准确的结果。