在任意键上使用EnumSet或EnumMap

时间:2015-02-18 04:09:24

标签: java performance enums

由于位操作的强大功能,我们知道EnumSetEnumMapHashSet / HashMap更快。但是,当真正重要时,我们是否真正利用EnumSet / EnumMap的真正力量?如果我们有一组数百万的记录,并且我们想知道该集合中是否存在某个对象,我们是否可以利用EnumSet的速度?

我查了一下,但没有发现任何讨论这个的事情。在任何地方都可以找到通常的东西,即因为EnumSetEnumMap使用一组预定义的密钥,对小集合的查找速度非常快。我知道枚举是编译时常量,但是我们可以兼顾两个世界 - EnumSet - 类似数据结构而不需要枚举作为键吗?

1 个答案:

答案 0 :(得分:2)

有趣的见解;简短的回答是否定的,但你的问题是探索一些我将尝试讨论的好的数据结构设计概念。

首先,让我们谈谈HashMapHashSet在内部使用HashMap,以便他们分享大多数行为);基于哈希的数据结构非常强大,因为它快速且通用。它很快(即大约O(1))因为我们可以通过非常少量的计算找到我们正在寻找的密钥。粗略地说,我们有一个键列表数组,将键转换为整数索引到该数组,然后查看键的关联列表。随着映射变大,反复调整后备阵列以容纳更多列表。假设列表均匀分布,则此查找非常快。因为这适用于任何通用对象(具有适当的.hashcode().equals()),它对几乎任何应用程序都很有用。

枚举有几个有趣的属性,但为了有效查找,我们只关心其中两个 - 它们通常很小,并且它们具有固定数量的值。因此,我们可以做得比HashMap更好;具体来说,我们可以将每个可能的值映射到一个唯一的整数,这意味着我们不需要计算哈希,我们也不需要担心哈希冲突。所以EnumMap只存储一个与枚举大小相同的数组,并直接查找它:

// From Java 7's EnumMap
public V get(Object key) {
    return (isValidKey(key) ?
            unmaskNull(vals[((Enum)key).ordinal()]) : null);
}

剥离一些必要的Map健全性检查,它只是:

return vals[key.ordinal()];

请注意,在概念上与标准HashMap没有区别,它只是避免了一些计算。 EnumSet稍微聪明一点,使用一个或多个long中的位来表示数组索引,但在功能上它与EnumMap情况没有区别 - 我们分配了足够的空位来覆盖所有可能的枚举值,可以使用它们的整数.ordinal()而不是计算哈希值。

那么HashMapEnumMap快多少?它显然更快,但实际上它 的速度要快得多。 HashMap已经是一种非常有效的数据结构,因此对其进行任何优化只会产生略微更好的结果。特别是,HashMapEnumMap渐近相同的速度(O(1)),意味着它们变大,它们表现得同样好。这是没有像EnumMap这样更通用的数据结构的主要原因 - 因为相对于HashMap而言,这是不值得的。

我们不想要更通用的“FiniteKeysMap”的第二个原因是它会让我们的生活变得更加复杂,如果它显着提高速度,那将是值得的,但是因为它不会只是麻烦。我们必须为可能是此映射中的键的任何类型定义接口(可能还有factory pattern)。该接口需要保证每个唯一实例返回[0-n)范围内的唯一哈希码,并为地图提供获取n和可能所有n元素的方法。最后两个操作作为静态方法会更好,但由于我们无法在接口中定义静态方法,因此它们必须直接传递给我们创建的每个映射,或者具有此信息的单独工厂对象将具有存在并传递给地图/设置在建设中。因为枚举是语言的一部分,所以它们免费获得所有这些好处,这意味着最终用户程序员不需要利用这些成本。

此外,使用此界面很容易出错;假设您的类型具有完全100,000个唯一值。它应该实现我们的界面吗?它可以。但你实际上可能会在脚下射击自己。这会占用大量不必要的内存,因为我们的FiniteKeysMap会分配一个新的100,000长度数组来表示一个空映射。一般来说,这种浪费的空间不值得这样的数据结构提供的边际改进。

简而言之,虽然你的想法是可能的,但这是不切实际的。 HashMap非常有效,试图为极少数情况创建单独的数据结构会增加复杂性而不是值。


对于更快.contains()次检查的具体情况,您可能希望Bloom Filters。它是一个类似集合的数据结构,非常有效地存储非常大的集合,条件是它有时可能错误地说元素在集合中不存在(但不是相反 - 如果它说元素不是在集合中,它绝对不是)。 Guava提供了一个很好的BloomFilter实现。