Effective Java笔记第五章枚举和注解
第四节用EnumMap代替序数索引
1.我们举个例子:
public class Herb {
public enum Type {
ANNUAL, PERENNIAL, BIENNIAL;
}
private final String name;
public final Type type;
public Herb(String name, Type type) {
this.name = name;
this.type = type;
}
@Override
public String toString() {
return name;
}
}
我们先用ordinal方法作为索引将Herb类按照Type进行分类。
public static void main(String[] args) {
//Using ordinal() to index an array - DO NOT DO THIS 使用ordinal()索引数组-不要这样做
//这是一个放有Herb对象的数组
Herb[] garden = {new Herb("annul", Herb.Type.ANNUAL),
new Herb("perennial", Herb.Type.BIENNIAL),
new Herb("biennial", Herb.Type.PERENNIAL),
new Herb("annul2", Herb.Type.ANNUAL)};
Set<Herb>[] herbs = //Indexed by Herb.Type.ordinal() 这是一个放Herb集合的数组,每一个元素都是一个Herb集合
(Set<Herb>[]) new Set[Herb.Type.values().length];
for (int i = 0; i < herbs.length; i++) {
//有几个枚举类型就创建几个集合
herbs[i] = new HashSet<Herb>();
}
for (Herb herb : garden) {
//根据不同的枚举类型把Herb[]数组中的Herb对象放入不同的Set<herb>集合中
herbs[herb.type.ordinal()].add(herb);
}
for (int i = 0; i < herbs.length; i++) {
System.out.printf("%s: %s%n", Herb.Type.values()[i], herbs[i]);
}
}
这种方法确实可行,但是有许多隐藏的问题。因为数组不能和泛型兼容,程序需要进行未受检的转换,并且不能正确无误的进行编译。因为数组不知道他的索引代表着什么,你必须手工标注这些索引的输出。最严重的问题在于,当你访问一个按照枚举的序数进行索引的数组时,使用正确的int值就是你的职责了,int不能提供枚举的类型安全。
对此,我们有一种更好的方法。上面的代码中数组实际上充当着从枚举到值的映射,有一种非常快速的Map实现专门用于枚举键,称作java.util.EnumMap。下面是用EnumMap改写后的代码:
public static void main(String[] args) {
//Using ordinal() to index an array - DO NOT DO THIS 使用ordinal()索引数组-不要这样做
//这是一个放有Herb对象的数组
Herb[] garden = {new Herb("annul", Herb.Type.ANNUAL),
new Herb("perennial", Herb.Type.BIENNIAL),
new Herb("biennial", Herb.Type.PERENNIAL),
new Herb("annul2", Herb.Type.ANNUAL)};
//对上面的代码进行改进
//用枚举作为key,以Herb集合作为value
Map<Herb.Type,Set<Herb>> herbsByType=new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
for (Herb.Type value : Herb.Type.values()) {
herbsByType.put(value,new HashSet<Herb>());
}
for (Herb herb : garden) {
herbsByType.get(herb.type).add(herb);
}
System.out.println(herbsByType);
}
这段程序更简洁,更清楚,也更加安全,运行速度方面可以与使用序数的程序相媲美。EnumMap在运行速度方面之所以能与通过序数索引的数组相媲美,是因为EnumMap在内部使用了这种数组。但是它对程序员隐藏了这种实现细节,集Map的丰富功能和类型安全与数组的快速于一身。注意EnumMap的构造器采用了键类型的Class对象:这是一个有限制的类型令牌,他提供了运行时的泛型信息。
2.你还可能见到按照序数进行索引(两次)的数组的数组,该序数表示两个枚举值的映射。下面我们举个例子:
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME},
{FREEZE, null, BOIL},
{DEPOSIT, CONDENSE, null}
};
public static Transition from(Phase src, Phase dst) {
return TRANSITIONS[src.ordinal()][dst.ordinal()];
}
}
}
public class PhaseTest {
public static void main(String[] args) {
Phase.Transition from = Phase.Transition.from(Phase.GAS, Phase.SOLID);
System.out.println(from);
}
}
这段程序可行,但是和之前的例子有着相同的缺陷,编译器无法知道序数和数组索引之间的关系,如果在过渡表中出了错,或者修改Phase或者Phase.Transition时忘记将他更新,程序运行时就会失败。下面我们使用EnumMap进行改编:
public enum PhaseImprove {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLTIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final PhaseImprove src;
private final PhaseImprove dst;
Transition(PhaseImprove src, PhaseImprove dst) {
this.src = src;
this.dst = dst;
}
//Initialize the phase transition map
private static final Map<PhaseImprove,Map<PhaseImprove,Transition>> m=new EnumMap<>(PhaseImprove.class);
static {
for (PhaseImprove value : PhaseImprove.values()) {
m.put(value,new EnumMap<PhaseImprove,Transition>(PhaseImprove.class));
}
for (Transition value : Transition.values()) {
m.get(value.src).put(value.dst,value);
}
}
public static Transition from(PhaseImprove src,PhaseImprove dst){
return m.get(src).get(dst);
}
}
}
public class PhaseImproveTest {
public static void main(String[] args) {
PhaseImprove.Transition from = PhaseImprove.Transition.from(PhaseImprove.GAS, PhaseImprove.LIQUID);
System.out.println(from);
}
}
3.总之,最好不要用序数来索引数组,而要使用EnumMap。如果你所表示的这种关系是多维的,就使用
EnumMap< … , EnumMap< … > >。