Java:通过HashMap迭代,哪个更有效?

时间:2011-04-28 23:52:05

标签: java map performance hashmap iteration

给出以下代码,有两种替代方法来迭代它,
这两种方法之间是否有任何性能差异?

        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的更有效方法(但我可能错了)

7 个答案:

答案 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.forEachHashMap.entrySet().forEach()在大型地图上表现最佳,并且entrySet()上的for循环加入,在小型地图上具有最佳性能。

keys方法较慢的原因可能是因为它们必须为每个条目再次查找值,而其他方法仅需要读取对象中已经获取值的字段。我希望迭代器方法变慢的原因是它们正在进行外部迭代,这需要对每个元素进行两个方法调用(hasNextnext),并将迭代状态存储在迭代器对象,而forEach完成的内部迭代只需要调用accept的一个方法即可。

您应该使用目标数据在目标硬件上进行概要分析,并在循环中执行目标操作,以获得更准确的结果。