以下为一个烹饪用的香草类:

    1. public class Herb {
    2. public enum Type {
    3. ANNUAL("一年生"), BIENNIAL("两年生"), PERENNTAL("多年生")
    4. private final String type;
    5. Type(String type) {
    6. this.type = type;
    7. }
    8. }
    9. // private static final Herb Type = null;
    10. private final String name;
    11. private final Type type;
    12. Herb(String name, Type type) {
    13. this.name = name;
    14. this.type = type;
    15. }
    16. @Override
    17. public String toString() {
    18. return name;
    19. }
    20. }}

    现有一个花园种草满了各种香草,要求对这些香草进行分类:把同一种的类型的香草分在同一组。

    • 使用泛型数组:

      1. // 数组索引顺序与枚举元素的序数强依赖
      2. Herb[] garden = ...
      3. Set<Herb>[] herbsByType
      4. = (Set<Herb>[])new Set[Herb.Type.values().length];
      5. for(int i = 0;i<herbsByType.length;i++){
      6. herbsByType[i] = new HashSet<Herb>();
      7. }
      8. for(Herb h:garden){
      9. herbsByType[h.type.ordinal()].add(h);
      10. }

      评价:这种方法是可行的,但是隐藏着很多问题。

      一是数组不应该与泛型混用,程序需要进行未受检的的转换,并且不能准确无误的进行编译。**

    未受检转换是什么意思?因为数组是协变的、可具体化的。所以在编译的时候并不能发现类型转换的错误,只有在运行的时候才会报错,这是数组设计的一个遗憾。如以下代码:

    1. Object[] objs = new String[10];
    2. objs[0] = new Object();
    3. 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登场:

      1. Herb[] garden = ...
      2. Map<Herb.Type, Set<Herb>> herbsByType
      3. = new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);
      4. for(Herb.Type t:Herb.Type.values()){
      5. herbsByType.put(t, new HashSet<Herb>());
      6. }
      7. for(Herb h:garden)
      8. herbsByType.get(h.type).add(h);
      9. System.out.println(herbsByType);

    简单明了,一个香草类型对应该类型的香草分组。

    好处:这段程序简短、更清楚、也更加安全,运行速度方面可以与使用序数的程序相媲美。EnumMap之所以速度媲美使用序数的程序,是因为EnumMap底层使用了这种数组。另外,集Map丰富功能和类型安全与数组的快速于一身。

    注意EnumMap构造器采用了键类型的Class对象:这是一个有限制的类型令牌,他提供了运行时的泛型信息。


    练习题:
    **
    一个物体有固体、液体、气体三种物理状态。现在设计一个查询,输入初始状态和输出状态获取一种物理状态变化行为,如液化、气化等等。

    以下是利用索引进行分类的过程:

    1. public enum Phase {
    2. SOLID, LIQUID, GAS;
    3. public enum Transition {
    4. MELT, FREEZE, BOTL, CONDENSE, SUBLIME, DEPOSIT;
    5. private static final Transition[][] TRANSITIONS = {
    6. {null, MELT, SUBLIME},// 开始状态为SOLID
    7. {FREEZE, null, BOTL},
    8. {DEPOSIT, CONDENSE, null}
    9. };
    10. public static Transition from(Phase src, Phase dst) {
    11. return TRANSITIONS[src.ordinal()][dst.ordinal()];
    12. }
    13. }
    14. }

    看起来很优雅、很巧妙,实质很蠢。这种方法非常依赖于Phase枚举常量的顺序,万一顺序发生改变,而Transition没有做出相应的修改(即使修改也很麻烦),那么将会返回错误的结果。

    改进
    使用双重EnumMap:

    1. public enum Phase {
    2. SOLID, LIQUID, GAS;
    3. public enum Transition {
    4. MELT(SOLID,LIQUID), FREEZE(LIQUID, SOLID),
    5. BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
    6. SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
    7. private final Phase src;
    8. private final Phase dst;
    9. Transition(Phase src, Phase dst) {
    10. this.src = src;
    11. this.dst = dst;
    12. }
    13. private static final Map<Phase, Map<Phase, Transition>> m =
    14. new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);
    15. static {
    16. for(Phase p : Phase.values())
    17. m.put(p, new EnumMap<Phase, Transition>(Phase.class));
    18. for(Transition t : Transition.values())
    19. m.get(t.src).put(t.dst, t);
    20. }
    21. public static Transition from(Phase src, Phase dst) {
    22. return m.get(src).get(dst);
    23. }
    24. }
    25. }

    总结:最好不要用序数来索引数组 。如果你所表示的这种关系是多维的 , 就使用 EnumMap<…,EnumMap<…>> 。代码中一般情况下都不使用 Enum.ordinal , 即使要用也是因为特殊情况。