EnumSet可能是Java中最无意义的类 - nullprogram


numSet 可能是 Java 标准库中最无意义的类。它有两个目标,但都失败了。
 
背景
在过去,Java 程序员会像 C 程序员一样构建枚举。例如,这是一个 C 风格的位域枚举(这些罗马人喜欢他们的关键字):

public final class Color {
    public static final int R = 1 << 0;
    public static final int G = 1 << 1;
    public static final int B = 1 << 2;
}

要构建一组这些项目,请使用 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 void benchmarkEnumSet() {
    System.gc();
    long beg = System.nanoTime();
    Set<Flag> a = EnumSet.of(Flag.A, Flag.B, Flag.G);
    for (int i = 0; i < 1_000_000_000; i++) {
        Set<Flag> b = EnumSet.of(Flag.A, Flag.B, Flag.G);
        assert a.equals(b);
    }
    long end = System.nanoTime();
    System.out.println(
"EnumSet\t" + (end - beg)/1e9);
}

经典位域的基准:

static final int A = 1 << 0;
static final int B = 1 << 1;
static final int C = 1 << 2;
static final int D = 1 << 3;
static final int E = 1 << 4;
static final int F = 1 << 5;
static final int G = 1 << 6;

// ...

static void benchmarkBitfield() {
    System.gc();
    long beg = System.nanoTime();
    int a = A | B | G;
    for (int i = 0; i < 1_000_000_000; i++) {
        int b = A | B | G;
        assert a == b;
    }
    long end = System.nanoTime();
    System.out.println(
"bitfield\t" + (end - beg)/1e9);
}


完整来源:Benchmark.java
在 x86-64 Debian Buster 及其 OpenJDK 11(OpenJDK 的最新长期支持版本)上的结果:

HashSet    104.486260884
EnumSet      3.900099588
bitfield     0.003371834

HashSet    109.827488593
EnumSet      3.484818891
bitfield     0.003430366

HashSet    107.106317379
EnumSet      3.742689517
bitield      0.000000057

EnumSet 比 HashSet 快两个数量级。这听起来不错,直到下一个结果:在最坏的情况下,位域比 EnumSet 快三个数量级。
。编译器未能在 HashSet 和 EnumSet 基准测试中实现相同的优化。一旦回暖,位域是 超过1000倍的速度更快,因为他们将不会阻止优化。
 
EnumSet 有什么意义呢
那么 EnumSet 有什么意义呢?我提到它没有实现它的两个目标中的任何一个。
  1. 位域缺乏类型安全性。Set(通过泛型)没有。
  2. 通常的 Set 实现 HashSet 远不如位域有效。EnumSet 尝试将其与位域恢复一致。

EnumSet 对于类型安全来说是不必要的,因为它已经是 Set 的一个属性。我们已经有了一个更通用的 Set 实现:HashSet。相对于位域,EnumSet 并不比 HashSet 快。
位域是一个原始类型,不会创建对象,当然Set 的类型安全需要付出高昂的代价。
这并不是说您应该改变编写 Java 的方式。这是对生态系统的批评——它的设计和习语——也是我没有错过它的(许多)原​​因之一。