以下为一个烹饪用的香草类:
public class Herb {
public enum Type {
ANNUAL("一年生"), BIENNIAL("两年生"), PERENNTAL("多年生")
private final String type;
Type(String type) {
this.type = type;
}
}
// private static final Herb Type = null;
private final String name;
private final Type type;
Herb(String name, Type type) {
this.name = name;
this.type = type;
}
@Override
public String toString() {
return name;
}
}}
现有一个花园种草满了各种香草,要求对这些香草进行分类:把同一种的类型的香草分在同一组。
使用泛型数组:
// 数组索引顺序与枚举元素的序数强依赖
Herb[] garden = ...
Set<Herb>[] herbsByType
= (Set<Herb>[])new Set[Herb.Type.values().length];
for(int i = 0;i<herbsByType.length;i++){
herbsByType[i] = new HashSet<Herb>();
}
for(Herb h:garden){
herbsByType[h.type.ordinal()].add(h);
}
评价:这种方法是可行的,但是隐藏着很多问题。
一是数组不应该与泛型混用,程序需要进行未受检的的转换,并且不能准确无误的进行编译。**
未受检转换是什么意思?因为数组是协变的、可具体化的。所以在编译的时候并不能发现类型转换的错误,只有在运行的时候才会报错,这是数组设计的一个遗憾。如以下代码:
Object[] objs = new String[10];
objs[0] = new Object();
System.out.println(objs[0]);
但是泛型是具有类型安全性,如果混用的话,就会失去类型安全性。如Set<Herb>[] herbsByType = (Set<Herb>[])new Set[Herb.Type.values().length];
,这个Set数组是可以保存非Set
二是,若要输出这些索引有意义必须手动标注,因为数组索引本身并没有与枚举常量相关的信息。
三,当然,用列表替代数组也可以解决混用问题。但是该分类问题实际是因为映射问题,而不是索引问题,int并没有类型安全性,如1可以索引 ANNUAL 类型香草分组,也可以用来索引 BIENNIAL 类型香草分组,所以int索引和类型分组之间并没有什么映射关系,如果输入错误的int值,可能会索引到错误的类型分组呢,但是程序并不会报错。那么为何不用更加合适的Map数据结构呢?!
EnumMap登场:
Herb[] garden = ...
Map<Herb.Type, Set<Herb>> herbsByType
= new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);
for(Herb.Type t:Herb.Type.values()){
herbsByType.put(t, new HashSet<Herb>());
}
for(Herb h:garden)
herbsByType.get(h.type).add(h);
System.out.println(herbsByType);
简单明了,一个香草类型对应该类型的香草分组。
好处:这段程序简短、更清楚、也更加安全,运行速度方面可以与使用序数的程序相媲美。EnumMap之所以速度媲美使用序数的程序,是因为EnumMap底层使用了这种数组。另外,集Map丰富功能和类型安全与数组的快速于一身。
注意EnumMap构造器采用了键类型的Class对象:这是一个有限制的类型令牌,他提供了运行时的泛型信息。
练习题:
**
一个物体有固体、液体、气体三种物理状态。现在设计一个查询,输入初始状态和输出状态获取一种物理状态变化行为,如液化、气化等等。
以下是利用索引进行分类的过程:
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOTL, CONDENSE, SUBLIME, DEPOSIT;
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME},// 开始状态为SOLID
{FREEZE, null, BOTL},
{DEPOSIT, CONDENSE, null}
};
public static Transition from(Phase src, Phase dst) {
return TRANSITIONS[src.ordinal()][dst.ordinal()];
}
}
}
看起来很优雅、很巧妙,实质很蠢。这种方法非常依赖于Phase枚举常量的顺序,万一顺序发生改变,而Transition没有做出相应的修改(即使修改也很麻烦),那么将会返回错误的结果。
改进
使用双重EnumMap:
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID,LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase src;
private final Phase dst;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
private static final Map<Phase, Map<Phase, Transition>> m =
new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);
static {
for(Phase p : Phase.values())
m.put(p, new EnumMap<Phase, Transition>(Phase.class));
for(Transition t : Transition.values())
m.get(t.src).put(t.dst, t);
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
}
总结:最好不要用序数来索引数组 。如果你所表示的这种关系是多维的 , 就使用 EnumMap<…,EnumMap<…>> 。代码中一般情况下都不使用 Enum.ordinal , 即使要用也是因为特殊情况。