通过有效地连接几个Java Map键集来迭代

时间:2011-06-29 08:31:28

标签: java map key set union

在我的一个Java 6项目中,我有一个LinkedHashMap实例数组作为一个方法的输入,该方法必须迭代所有键(即通过所有映射的键集的并集)并使用相关值。并非所有映射中都存在所有键,并且该方法不应多次遍历每个键或更改输入映射。

我目前的实现如下:

Set<Object> keyset = new HashSet<Object>();

for (Map<Object, Object> map : input) {
    for (Object key : map.keySet()) {
        if (keyset.add(key)) {
            ...
        }
    }
}

HashSet实例确保不会多次执行任何操作。

不幸的是,这部分代码在性能方面非常关键,因为它经常被称为非常。事实上,根据分析器,超过10%的CPU时间花费在HashSet.add()方法上。

我正在尽可能地优化这些代码。使用LinkedHashMap及其更有效的迭代器(与普通HashMap相比)是一个显着的提升,但我希望将基本上的簿记时间减少到最小。

事先将所有密钥放在HashSet中,使用addAll()被证明效率较低,因为之后调用HashSet.contains()的成本较高。 目前我正在研究是否可以使用位图(好吧,确切地说boolean[])来完全避免使用HashSet,但根据我的键范围,它可能根本不可能。

有更有效的方法吗?最好是不会对钥匙造成限制的东西吗?

编辑:

一些澄清和评论:

  • 我确实需要所有来自地图的值 - 我不能删除它们中的任何一个。

  • 我还需要知道每个值来自哪个地图。我的代码中缺少的部分(...)将是这样的:

    for (Map<Object, Object> m : input) {
        Object v = m.get(key);
    
        // Do something with v
    }
    

    一个简单的例子来了解我需要对地图做些什么,就像这样并行打印所有地图:

    Key Map0 Map1 Map2
    F   1    null 2
    B   2    3    null
    C   null null 5
    ...
    

    这不是我实际做的,但你应该明白这一点。

  • 输入地图是变量。实际上,此方法的每次调用都使用不同的一组。因此,我不会通过缓存他们的密钥联合来获得任何东西。

  • 我的密钥都是String实例。它们使用单​​独的HashMap在堆上进行实例化,因为它们非常重复,因此它们的哈希代码已经被缓存并且大多数哈希验证(当HashMap实现在哈希代码之后检查两个键是否实际相等时)匹配)归结为身份比较(==)。分析器确认只有0.5%的CPU时间花费在String.equals()String.hashCode()上。

编辑2:

根据答案中的建议,我做了一些测试,分析和基准测试。最终我的性能提升了大约7%。我做了什么:

  • 我将HashSet的初始容量设置为所有输入映射的集合大小的两倍。通过消除HashSet中的大多数(全部?)resize()调用,这在1-2%的范围内获得了一些东西。

  • 我使用Map.entrySet()作为我正在迭代的地图。由于额外的代码以及担心额外的检查和Map.Entry getter方法调用将超过任何优点,我最初避免使用这种方法。事实证明,整体代码稍快一些。

  • 我相信有些人会开始尖叫我,但这里是:原始类型。更具体地说,我在上面的代码中使用了原始形式的HashSet。由于我已经使用Object作为其内容类型,因此我不会失去任何类型的安全性。调用checkcast时无用的HashSet.add()操作的成本显然非常重要,足以在移除时将性能提高4%。为什么JVM坚持检查转换为Object超出了我的范围......

4 个答案:

答案 0 :(得分:2)

无法替代您的方法,但有一些建议(略微)优化现有代码。

  1. 考虑使用容量(所有映射的大小总和)初始化哈希集。这可以避免/减少添加操作期间集的大小调整
  2. 考虑不使用keySet(),因为它总是会在后台创建一个新集。使用entrySet(),这应该快得多
  3. 查看equals()hashCode()的实施 - 如果它们“昂贵”,那么您会对add方法产生负面影响。

答案 1 :(得分:1)

如何避免使用HashSet取决于您正在做什么。

每次更改input时,我只计算一次联合。对于查找次数,这应该是相对罕见的。

// on an update.
Map<Key, Value> union = new LinkedHashMap<Key, Value>();
for (Map<Key, Value> map : input) 
    union.putAll(map);


// on a lookup.
Value value = union.get(key);
// process each key once
for(Entry<Key, Value> entry: union) {
   // do something.
}

答案 2 :(得分:0)

选项A是使用.values()方法并迭代它。但我想你已经想到了它。

如果经常调用代码,则可能值得创建其他结构(取决于数据更改的频率)。创建一个新的HashMap;任何一个哈希映射中的每个键都是这个键中的一个键,该列表使HashMaps保持在该键出现的位置。

如果数据有点静态(与查询频率有关),这将有所帮助,因此管理结构的过载相对较小,并且如果密钥空间不是很密集(密钥不会重复很多)不同的HashMaps),因为它会节省很多不需要的contains()。

当然,如果要混合数据结构,最好将所有数据结构封装在自己的数据结构中。

答案 3 :(得分:0)