Effective Java 3rd 中文版(item:36-37)

36. 使用EnumSet替代位属性

如果枚举类型的元素主要用于集合中,一般来说使用int枚举模式(条目 34),下面将2的不同倍数赋值给每个常量:

1
2
3
4
5
6
7
8
9
10
// 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)的集合中:

1
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

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

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

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

1
2
3
4
5
6
7
// 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类提供了一组丰富的静态工厂,可以轻松创建集合,其中一个代码如下所示:

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

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

总之,仅仅因为枚举类型将被用于集合中,所以没有理由用位属性来表示它EnumSet类将位属性的简洁性和性能与条目 34中所述的枚举类型的所有优点相结合。EnumSet的一个真正缺点是,它不像Java 9那样创建一个不可变的EnumSet,但是在即将发布的版本中可能会得到补救。 同时,你可以用Collections.unmodifiableSet封装一个EnumSet,但是简洁性和性能会受到影响。

37. 使用EnumMap替代序数索引

有时可能会看到使用ordinal方法(条目 35)来索引到数组或列表的代码。 例如,考虑一下这个简单的类来代表一种植物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;

Plant(String name, LifeCycle lifeCycle) {
[this.name](http://this.name) = name;
this.lifeCycle = lifeCycle;
}

@Override public String toString() {
return name;
}
}

现在假设你有一组植物代表一个花园,想要列出这些由生命周期组织的植物(一年生,多年生,或双年生)。为此,需要构建三个集合,每个生命周期作为一个,并遍历整个花园,将每个植物放置在适当的集合中。一些程序员可以通过将这些集合放入一个由生命周期序数索引的数组中来实现这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Using ordinal() to index into an array - DON'T DO THIS!

Set<Plant>[] plantsByLifeCycle =

(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];

for (int i = 0; i < plantsByLifeCycle.length; i++)

plantsByLifeCycle[i] = new HashSet<>();

for (Plant p : garden)

plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);

// Print the results

for (int i = 0; i < plantsByLifeCycle.length; i++) {

System.out.printf("%s: %s%n",

Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);

}

这种方法是有效的,但充满了问题。 因为数组不兼容泛型(条目 28),程序需要一个未经检查的转换,并且不会干净地编译。 由于该数组不知道索引代表什么,因此必须手动标记索引输出。 但是这种技术最严重的问题是,当你访问一个由枚举序数索引的数组时,你有责任使用正确的int值; int不提供枚举的类型安全性。 如果你使用了错误的值,程序会默默地做错误的事情,如果你幸运的话,抛出一个ArrayIndexOutOfBoundsException异常。

有一个更好的方法来达到同样的效果。 该数组有效地用作从枚举到值的映射,因此不妨使用Map。 更具体地说,有一个非常快速的Map实现,设计用于枚举键,称为java.util.EnumMap。 下面是当程序重写为使用EnumMap时的样子:

1
2
3
4
5
// Using an EnumMap to associate data with an enum

Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =

new EnumMap<>(Plant.LifeCycle.class);