第32条:用EnumSet
代替位域
如果一个枚举类型的元素主要用在集合中,一般就是用int
枚举模式(见第30条),将 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);
位域表示法也允许利用位操作,有效地执行像 union(联合)和 intersection(交集)这样的集合操作。但位域有着int
枚举常量的所有缺点,甚至更多。当位域以数字形式打印时,翻译位域比翻译简单的int
枚举常量要困难得多。甚至,要遍历位域表示的所有元素也没有很容易的方法。
有些程序员优先使用枚举而非int
常量,他们在需要传递多组常量集时,仍然倾向于使用位域。其实没有理由这么做,因为还有更好的替代方法。java.util
包提供了EnumSet
类来有效地表示从单个枚举类型中提取的多个值的多个集合。这个类实现Set
接口,提供了丰富的功能、类型安全性,以及可以从任何其他Set
实现中得到的互用性。但是在内部具体的实现上,每个EnumSet
内容都表示为位矢量。如果底层的枚举类型有64个或者更少的元素——大多如此——整个EnumSet
就是用单个long
来表示,因此它的性能比得上位域的性能。批处理,如removeAll
和retainAll
,都是利用位算法来实现的,就像手工替位域实现得那样。但是可以避免手工操作时容易出现的错误以及不太雅观的代码,因为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<Styles>
而非EnumSet<Style>
。虽然看起来好像所有的客户端都可以将EnumSet
传递到这个方法,但是最好还是接受接口类型而非接受实现类型。这是考虑到可能会有特殊的客户端要传递一些其他的Set
实现,并且没有什么明显的缺点。
总而言之,正式因为枚举类型要用在集合(Set)中,所以没有利用用位域来表示它。EnumSet
类集位域的简洁和性能优势及第 30 条中所述的枚举类型的所有优点于一身。实际上EnumSet
有个缺点,即截止 Java 1.6 发行版本,它都无法创建不可变的EnumSet
,但是这一点很可能在即将出来的版本中得到修正。同时,可以用Collections.unmodifiableSet
将EnumSet
封装起来,但是简洁性和性能会受到影响。