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&lt;K,V&gt;

您的第一个 sn-p 丢弃条目中的值(在 KeyIterator 中),然后在字典中再次查找它。

您的第二个 sn-p 直接使用键和值(来自 EntryIterator

keySet()entrySet() 都是廉价电话)

【讨论】:

【参考方案3】:

地图:

Map&lt;String, Integer&gt; map = new HashMap&lt;String, Integer&gt;();

除了这两个选项,还有一个。

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.forEachHashMap.entrySet().forEach() 在大地图上表现最佳,并通过 entrySet() 上的 for 循环加入,以获得在小地图上的最佳性能。

keys 方法较慢的原因可能是因为它们必须为每个条目再次查找值,而其他方法只需要读取他们已经必须获取值的对象中的字段。我希望迭代器方法变慢的原因是它们正在进行外部迭代,这需要对每个元素进行两次方法调用(hasNextnext),并将迭代状态存储在迭代器对象中,而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 进行迭代,哪个更高效?的主要内容,如果未能解决你的问题,请参考以下文章

java 通过HashMap迭代

在 Ember 1.13 及更高版本中,当迭代字符串数组时,我应该使用哪个键?

Java8 HashMap源码分析

Java基础中map接口和实现类

在 Java(或 Scala)中迭代 HashMap 的 HashMap

MongoDB vs MySQL,哪个效率更高?