Skip to main content

36. 使用 EnumSet 替代位属性

  如果枚举类型的元素主要用于集合中,则传统上使用 int 枚举模式(详见第 34 条),将 2 的不同次幂赋值给每个常量:

// Bit field enumeration constants - OBSOLETE!
public class Text {
    public static final int STYLE_BOLD          = 1 << 0;  // 1
    public static final int STYLE_ITALIC        = 1 << 1;  // 2
    public static final int STYLE_UNDERLINE     = 1 << 2;  // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3;  // 8

    // Parameter is bitwise OR of zero or more STYLE_ constants
    public void applyStyles(int styles) { ... }
}

  这种表示方式允许你使用按位或(or)运算将几个常量合并到一个称为位域(bit field)的集合中:

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

  位域表示还允许你使用按位算术有效地执行集合运算,如并集和交集。 但是位域具有 int 枚举常量等的所有缺点。 当打印为数字时,解释位域比简单的 int 枚举常量更难理解。 没有简单的方法遍历所有由位域表示的元素。 最后,必须预测在编写 API 时需要的最大位数,并相应地为位域(通常为 intlong)选择一种类型。 一旦你选择了一个类型,你就不能在不改变 API 的情况下超过它的宽度(32 或 64 位)。

  当需要传递一组常量时,一些优先使用枚举而不是 int 常量的程序员仍然坚持使用位域。 没有理由这样做,因为存在更好的选择。 java.util 包提供了 EnumSet 类来有效地表示从单个枚举类型中提取的值集合。 这个类实现了 Set 接口,提供了所有其他 Set 实现的丰富性,类型安全性和互操作性。 但是在内部,每个 EnumSet 都表示为一个位向量(bit vector)。 如果底层的枚举类型有 64 个或更少的元素,并且大多数情况下,整个 EnumSet 用单个 long 表示,所以它的性能与位域的性能相当。 批量操作(如 removeAllretainAll)是使用按位算术实现的,就像你手动操作位域一样。 但是完全避免了手动进行位操作导致的丑陋和潜在错误:EnumSet 为你完成这一困难工作。

  下面是前一个使用枚举和枚举集合替代位域的示例。 它更短,更清晰,更安全:

// EnumSet - a modern replacement for bit fields
public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

    // Any Set could be passed in, but EnumSet is clearly best
    public void applyStyles(Set<Style> styles) { ... }
}

  这里是将 EnumSet 实例传递给 applyStyles 方法的客户端代码。 EnumSet 类提供了一组丰富的静态工厂,可以轻松创建集合,其中一个代码如下所示:

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

  请注意,applyStyles 方法采用Set<Style>而不是EnumSet<Style>参数。 尽管所有客户端都可能会将 EnumSet 传递给该方法,但接受接口类型而不是实现类型通常是很好的做法(详见第 64 条)。 这允许一个不寻常的客户端通过其他 Set 实现的可能性。

  总之,仅仅因为枚举类型将被用于集合中,就没有理由用位域来表示它。 EnumSet 类结合了位域的简洁性和性能以及条目 34 中所述的枚举类型的所有优点。截至 Java 9 ,EnumSet 的一个真正缺点是不能创建一个不可变的 EnumSet,但是在之后的发行版本中这可能会得到纠正。 同时,你可以用 Collections.unmodifiableSet 封装一个 EnumSet,但是简洁性和性能会受到影响。