由于位操作的强大功能,我们知道EnumSet
和EnumMap
比HashSet
/ HashMap
更快。但是,当真正重要时,我们是否真正利用EnumSet
/ EnumMap
的真正力量?如果我们有一组数百万的记录,并且我们想知道该集合中是否存在某个对象,我们是否可以利用EnumSet
的速度?
我查了一下,但没有发现任何讨论这个的事情。在任何地方都可以找到通常的东西,即因为EnumSet
和EnumMap
使用一组预定义的密钥,对小集合的查找速度非常快。我知道枚举是编译时常量,但是我们可以兼顾两个世界 - EnumSet
- 类似数据结构而不需要枚举作为键吗?
答案 0 :(得分:2)
有趣的见解;简短的回答是否定的,但你的问题是探索一些我将尝试讨论的好的数据结构设计概念。
首先,让我们谈谈HashMap
(HashSet
在内部使用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()
而不是计算哈希值。
那么HashMap
比EnumMap
快多少?它显然更快,但实际上它 的速度要快得多。 HashMap
已经是一种非常有效的数据结构,因此对其进行任何优化只会产生略微更好的结果。特别是,HashMap
和EnumMap
渐近相同的速度(O(1)
),意味着它们变大,它们表现得同样好。这是没有像EnumMap
这样更通用的数据结构的主要原因 - 因为相对于HashMap
而言,这是不值得的。
我们不想要更通用的“FiniteKeysMap
”的第二个原因是它会让我们的生活变得更加复杂,如果它显着提高速度,那将是值得的,但是因为它不会只是麻烦。我们必须为可能是此映射中的键的任何类型定义接口(可能还有factory pattern)。该接口需要保证每个唯一实例返回[0-n)
范围内的唯一哈希码,并为地图提供获取n
和可能所有n
元素的方法。最后两个操作作为静态方法会更好,但由于我们无法在接口中定义静态方法,因此它们必须直接传递给我们创建的每个映射,或者具有此信息的单独工厂对象将具有存在并传递给地图/设置在建设中。因为枚举是语言的一部分,所以它们免费获得所有这些好处,这意味着最终用户程序员不需要利用这些成本。
此外,使用此界面很容易出错;假设您的类型具有完全100,000
个唯一值。它应该实现我们的界面吗?它可以。但你实际上可能会在脚下射击自己。这会占用大量不必要的内存,因为我们的FiniteKeysMap
会分配一个新的100,000
长度数组来表示一个空映射。一般来说,这种浪费的空间不值得这样的数据结构提供的边际改进。
简而言之,虽然你的想法是可能的,但这是不切实际的。 HashMap
非常有效,试图为极少数情况创建单独的数据结构会增加复杂性而不是值。
对于更快.contains()
次检查的具体情况,您可能希望Bloom Filters。它是一个类似集合的数据结构,非常有效地存储非常大的集合,条件是它有时可能错误地说元素在集合中不存在(但不是相反 - 如果它说元素不是在集合中,它绝对不是)。 Guava提供了一个很好的BloomFilter
实现。