Enum.values()和EnumSet.allOf(),哪一个更可取?

69

我查看了 EnumSet.allOf 的内部实现,发现它非常高效,尤其是对于不到 64 个值的枚举类型。

基本上,所有的集合都共享一个包含所有可能的枚举值的数组,并且唯一的其他信息是一个位掩码,在 allOf 的情况下可以一次性设置。

另一方面,Enum.values() 看起来有点像黑魔法。此外,它返回的是一个数组,而不是一个集合,因此在许多情况下,必须使用 Arrays.asList() 进行装饰,以便在任何需要集合的地方使用。

那么,EnumSet.allOf 是否应该比 Enum.values 更可取呢?

更具体地说,应该使用哪种形式的 for 循环迭代器:

for ( final MyEnum val: MyEnum.values( ) );
或者
for ( final MyEnum val: EnumSet.allOf( MyEnum.class ) );
6个回答

99

因为我没有得到我的问题的答案,即哪个更有效率,所以我决定自己进行一些测试。

我测试了对 values(),Arrays.asList( values() )EnumSet.allOf( ) 进行迭代。 我针对不同的枚举大小重复了这些测试 10,000,000 次。这是测试结果:

oneValueEnum_testValues         1.328
oneValueEnum_testList           1.687
oneValueEnum_testEnumSet        0.578

TwoValuesEnum_testValues        1.360
TwoValuesEnum_testList          1.906
TwoValuesEnum_testEnumSet       0.797

ThreeValuesEnum_testValues      1.343
ThreeValuesEnum_testList        2.141
ThreeValuesEnum_testEnumSet     1.000

FourValuesEnum_testValues       1.375
FourValuesEnum_testList         2.359
FourValuesEnum_testEnumSet      1.219

TenValuesEnum_testValues        1.453
TenValuesEnum_testList          3.531
TenValuesEnum_testEnumSet       2.485

TwentyValuesEnum_testValues     1.656
TwentyValuesEnum_testList       5.578
TwentyValuesEnum_testEnumSet    4.750

FortyValuesEnum_testValues      2.016
FortyValuesEnum_testList        9.703
FortyValuesEnum_testEnumSet     9.266

这些是从命令行运行的测试结果。当我从Eclipse中运行这些测试时,testValues得到了压倒性的支持。即使对于小枚举值,它也比EnumSet更小。我相信这种性能提升来自于for (val : array)循环中数组迭代器的优化。

另一方面,一旦需要传递java.util.Collection,Arrays.asList()就输给了EnumSet.allOf,尤其是对于小枚举值,在任何给定的代码库中,我认为它们占大多数。

因此,我建议你使用:

for ( final MyEnum val: MyEnum.values( ) )

但是

Iterables.filter(
    EnumSet.allOf( MyEnum.class ),
    new Predicate< MyEnum >( ) {...}
)

仅在确实需要使用java.util.List的情况下,才使用Arrays.asList(MyEnum.values())


12

你应该选择对你来说最简单和最清晰的方法。在大多数情况下,性能不应是考虑因素。

个人认为:这两种方案都不太理想,因为它们都创建了对象。第一种情况下创建了一个对象,而第二种情况下创建了三个对象。你可以构造一个常量来保存所有值以提高性能。


10
现在是2010年,创建对象仍然不是免费的。对于大多数编程来说,创建对象并不重要,但如果性能确实很重要,你创建的对象数量仍然可能会产生影响。 - Peter Lawrey
4
我曾参与过一个项目,其中在关键路径上创建的每个对象每年的成本超过200美元。因此,在某些情境下,如果你进行多次操作,三个对象可能听起来很昂贵。请注意,这里不包括任何解释。 - Peter Lawrey
3
不要在循环中实例化对象,应该在循环外部实例化。 - Thufir

8

还有一个Class.getEnumConstants()方法。

它们底层都通过反射调用枚举类型的values()方法


1
这与我所问的问题有什么关联吗? - Alexander Pogrebnyak
2
这与问题有关,因为其他所有方法都在幕后使用values()。 - A myth

4
values()方法更加清晰和高效,如果您只想遍历所有可能的枚举值。这些值由类缓存(请参阅Class.getEnumConstants())。
如果您需要一部分值,则应使用EnumSet。从allOf()noneOf()开始添加或删除值,或根据需要仅使用of()

1
values() 无法被类缓存,因为它是一个数组,用户可以随意更改其值。因此,我怀疑它必须是一个克隆体。另一方面,EnumSet.allOf确实使用了一个共享的数组值,因此在这里肯定有更少的内存分配。所以,values可能更清晰,但我怀疑它的性能不会更好。 - Alexander Pogrebnyak
1
@Alexander:你说得对,数组被克隆了,但clone()是本地的。通过一些调试,我发现getEnumConstants()使用的是values(),而不是反过来。 - Arne Burmeister

2

虽然我没有完全实现,但在我看来,EnumSet.allOf() 基本上使用与 .values() 相同的基础设施。因此,我预计 EnumSet.allOf() 需要一些(可能是微不足道的)额外步骤(请参见 http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6276988)。

对我来说,foreach 的预期使用方式很明显是 for(MyEnum val : MyEnum.values()) 为什么要用不同的方法?这只会让维护程序员感到困惑。

我的意思是,如果你需要一个集合,你应该得到一个集合。如果你想使用 foreach,数组已经足够好了。如果被迫选择,我甚至更喜欢数组!如果你拥有的东西(数组)已经足够好了,为什么还要用其他东西包装它呢?简单的东西通常更快。

无论如何,Peter Lawrey 是对的。不要担心这个的性能。它足够快了,而且很有可能有成千上万的其他瓶颈,使得那个微小的理论性能差异完全无关紧要(不过我没有看到他的“对象创建”一点。对我来说,第一个例子似乎完全没问题)。


@Zwei:请看我对Arne帖子的评论。 - Alexander Pogrebnyak
@Alexander:好的,他们在JDK6中修复了这个bug(见链接)?嗯,我理解你的观点,但我依然认为对于你的问题“更确切地说,应该使用哪种形式的for循环迭代器”,我的回答是“使用第一个例子”。我的意思是,我不知道。如果你在嵌入式、实时应用程序或其他类似开发方面进行开发,也许可以证明使用第二个例子是有合理性的。但在普通的、一般的上下文中呢?没有。 - Enno Shioji

0

EnumSet 的设计并不是为了迭代其值,而是为了高效地(或者说相对高效地)表示位图或位掩码。EnumSet 的 Javadoc 也指出:

枚举集合在内部被表示为位向量。这种表示非常紧凑和高效。该类的空间和时间性能应该足够好,以允许它作为传统基于 int 的“位标志”的高质量、类型安全的替代品。即使是批量操作(如 containsAll 和 retainAll),如果它们的参数也是枚举集合,也应该运行得非常快。

由于只有一个单独的位可以表示某个枚举值,因此它也被实现为一个 Set 而不是一个 List

现在,使用 C 风格的位掩码(x^2)可能也可以实现同样的功能,并且速度更快,但使用枚举提供了更直观的编码风格和类型安全的用法,并且可以轻松扩展到超出 intlong 可以容纳的大小。

因此,您可以按以下方式测试所有位是否设置:

public class App {
  enum T {A,B}
  public static void main(String [] args) {
    EnumSet<T> t = EnumSet.of(T.A);
    t.containsAll(EnumSet.allOf(T.class));
  }
}

你完全搞错了。EnumSet首先是一个Collection,实际上是一个Set。由于枚举的属性,事实证明这种集合最有效的表示形式是位掩码。此外,请注意,你示例中的containsAll并不是EnumSet独有的,它是Set的一个方法。但是,撇开这些不谈,你没有回答最初发布的问题,当你需要访问枚举中的所有值时,哪种形式更有效。 - Alexander Pogrebnyak
@AlexanderPogrebnyak 请证实我所说的是否正确,因为我不太清楚。我从未声称EnumSet是除了Set之外的任何东西。因此,像containsAllretainAll这样的方法并不是EnumSet独有的,尽管它们具有完全独特的实现。至于答案,您似乎已经提供了一个很好的指标。我只是想补充一下,因为我认为选择不应该仅基于枚举值的完整集合迭代。 - YoYo
有时遍历完整的枚举值集合是你唯一的选择。例如,当你必须验证并分配外部传递的值到枚举时,而该值并不明确地映射到枚举名称,因此你无法使用 Enum.valueOf - Alexander Pogrebnyak
@AlexanderPogrebnyak 迭代一次以构建“Map”,然后使用该映射将您的“外部值”转换为enum常量。 您可以实现一个类似于内置valueOf方法的便捷方法。这种方法使得您最初的问题也变得无关紧要,因为构建地图应该只发生一次,作为静态初始化的一部分enum - YoYo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接