numSet 可能是 Java 标准库中最无意义的类。它有两个目标,但都失败了。
背景
在过去,Java 程序员会像 C 程序员一样构建枚举。例如,这是一个 C 风格的位域枚举(这些罗马人喜欢他们的关键字):
public final class Color { |
要构建一组这些项目,请使用 OR ( |) 运算符,就像 C 一样:
int purple = Color.R | Color.B;
缺点是缺乏类型安全。这些是普通的旧整数,不是专用类型,编译器不会检查整数和位域是否交叉。为了解决这个问题,早期的 Java 获得了一个枚举引用类型:
public enum Color { R, G, B }
值得庆幸的是,这只是比 C 枚举的相同语法稍微冗长一些。虽然它很有用,但这些类型不支持 OR 运算符。相反,您应该构建一个 Set。罗马人也爱仪式:
Set<Color> purple = new HashSet<>(Arrays.asList(Color.R, Color.B));
正如您可能猜到的,与原始整数运算相比,这个 HashSet 非常慢且效率低下。类型安全的代价是巨大的。为了缓解这种情况,Java 为枚举提供了一个特殊的 Set 实现:
Set<Color> purple = EnumSet.of(Color.R, Color.B); |
不像 OR 运算符那样在语法上精益求精,但比 HashSet 更简洁、更高效。效率来自内部使用位域,就像原始的预枚举示例一样。但它的效率有多高呢?
基准
EnumSet 是引用类型,而不是原始类型,因此创建 EnumSet 需要:
- 内存分配
- 运行构造函数
- 运行时构建
- 反射
这是个体元素思维的本质。没有理由认为 EnumSet 会高效。
为了了解相对成本,我整理了一些粗略的基准。在基准测试中,我构建了一组值,然后多次构建相同的集合并将其与原始集合进行比较。这是 EnumSet 基准测试:
enum Flag { A, B, C, D, E, F, G } |
经典位域的基准:
static final int A = 1 << 0; |
完整来源:Benchmark.java
在 x86-64 Debian Buster 及其 OpenJDK 11(OpenJDK 的最新长期支持版本)上的结果:
HashSet 104.486260884 |
EnumSet 比 HashSet 快两个数量级。这听起来不错,直到下一个结果:在最坏的情况下,位域比 EnumSet 快三个数量级。
。编译器未能在 HashSet 和 EnumSet 基准测试中实现相同的优化。一旦回暖,位域是 超过1000倍的速度更快,因为他们将不会阻止优化。
EnumSet 有什么意义呢
那么 EnumSet 有什么意义呢?我提到它没有实现它的两个目标中的任何一个。
- 位域缺乏类型安全性。Set(通过泛型)没有。
- 通常的 Set 实现 HashSet 远不如位域有效。EnumSet 尝试将其与位域恢复一致。
EnumSet 对于类型安全来说是不必要的,因为它已经是 Set 的一个属性。我们已经有了一个更通用的 Set 实现:HashSet。相对于位域,EnumSet 并不比 HashSet 快。
位域是一个原始类型,不会创建对象,当然Set 的类型安全需要付出高昂的代价。
这并不是说您应该改变编写 Java 的方式。这是对生态系统的批评——它的设计和习语——也是我没有错过它的(许多)原因之一。