《Java编程思想》笔记19-枚举类型

基本 enum 特性

  • 枚举型的实质是“单例模式”的变种,枚举型中的每个枚举都是它的一个静态成员字段。而且无法改变(常量)。
  • Enum 类是枚举的一个封装类,是所有枚举类型的超类,它是一个没有抽象方法的抽象类。Enum 类实现了 Comparable 接口,所以它具有 compareTo() 方法。同时它还实现了 Serializable 接口。
  • ordinal():获取枚举元素声明时的顺序,从 0 开始计算。
  • 可以使用“==”来比较两个枚举实例相同与否,由于编译器自动实现了 equals() 和 hashCode() 两个方法。
  • 调用 getDeclaringClass() 方法,我们就能知道其所属的 enum 类。
  • name():返回 enum 实例声明时的名字,与使用 toString() 方法效果相同。
  • 所有 enum 都继承了 Enum 类,所以 enum 不能再继承其他类,但是可以实现一个或多个接口。

向 enum 中添加新方法

除了不能继承自一个 enum 之外,可以将 enum 看作一个常规的类,也就是说可以向 enum 添加方法,甚至可以由 main 方法。

  • 如果打算添加自己的方法,那么必须在 enum 实例的最后添加一个分号,而且必须先定义 enum 实例,实例之前不能有任何方法,否则报错。

  • 只能在 enum 定义的内部使用其构造器创建 enum 实例,所以 enum 构造器声明为 private 并没有什么影响。

values() 的神秘之处

编译器自动合成了另外两个静态方法:values() 和 valueOf()。这两个是非常常用的用来获取枚举实例的手段:

  • values(): 返回含有全部内部枚举实例的数组。
  • valueOf(String name): 通过某个枚举实例的String字面量来获取与之对应的枚举实例。

如果将 enum 实例向上转型为 Enum,那么 values() 方法就不可访问了。不过,在 Class 中有一个 getEnumConstants() 方法,所以即便 Enum 接口中没有 values() 方法,我们仍然可以通过 Class 对象取得所有 enum 实例。

enum Search { HITHER, YON }

public class UpcastEnum {
    public static void main(String[] args) {
        Search[] vals = Search.values();
        Enum e = Search.HITHER; // Upcast
        // e.values(); // No values() in Enum
        for(Enum en : e.getClass().getEnumConstants())
            System.out.println(en);
    }
} /* Output:
HITHER
YON
*///:~

使用接口组织枚举

我们希望从 enum 继承子类,是由于有时我们希望扩展远 enum 中的元素,有时是因为我们希望使用子类将一个 enum 中的元素进行分组。

在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组,可以达到将枚举元素分类组织的目的。

public interface Food {
    enum Appetizer implements Food {
        SALAD, SOUP, SPRING_ROLLS;
    }
    enum MainCourse implements Food {
        LASAGNE, BURRITO, PAD_THAI,
        LENTILS, HUMMOUS, VINDALOO;
    }
    enum Dessert implements Food {
        TIRAMISU, GELATO, BLACK_FOREST_CAKE,
        FRUIT, CREME_CARAMEL;
    }
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
        LATTE, CAPPUCCINO, TEA, HERB_TEA;
    }
}

如果想创建一个“枚举的枚举”,那么可以创建一个新的 enum,然后用其实例包装 Food 中的每一个 enum 类。

public enum Course {
    APPETIZER(Food.Appetizer.class),
    MAINCOURSE(Food.MainCourse.class),
    DESSERT(Food.Dessert.class),
    COFFEE(Food.Coffee.class);
    private Food[] values;
    private Course(Class<? extends Food> kind) {
        values = kind.getEnumConstants();
    }
    public Food randomSelection() {
        return Enums.random(values);
    }
}

另一种管理枚举的方法

public enum Meal2 {
    APPETIZER(Food.Appetizer.class),
    MAINCOURSE(Food.MainCourse.class),
    DESSERT(Food.Dessert.class),
    COFFEE(Food.Coffee.class);
    private Food[] values;
    private Meal2(Class&lt;? extends Food> kind) {
        values = kind.getEnumConstants();
    }
    public interface Food {
        enum Appetizer implements Food {
            SALAD, SOUP, SPRING_ROLLS;
        }
        enum MainCourse implements Food {
            LASAGNE, BURRITO, PAD_THAI,
            LENTILS, HUMMOUS, VINDALOO;
        }
        enum Dessert implements Food {
            TIRAMISU, GELATO, BLACK_FOREST_CAKE,
            FRUIT, CREME_CARAMEL;
        }
        enum Coffee implements Food {
            BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
            LATTE, CAPPUCCINO, TEA, HERB_TEA;
        }
    }
    public Food randomSelection() {
        return Enums.random(values);
    }
    public static void main(String[] args) {
        for(int i = 0; i &lt; 5; i++) {
            for(Meal2 meal : Meal2.values()) {
                Food food = meal.randomSelection();
                System.out.println(food);
            }
            System.out.println("---");
        }
    }
}

EnumSet

函数名 说明
allOf(Class<E> elementType) 创建一个包含指定元素类型的所有元素的枚举 set。
complementOf(EnumSet<E> s) 创建一个其元素类型与指定枚举 set 相同的枚举 set,最初包含指定 set 中所不 包含的此类型的所有元素。
copyOf(Collection<E> c) 创建一个从指定 collection 初始化的枚举 set。
copyOf(EnumSet<E> s) 创建一个其元素类型与指定枚举 set 相同的枚举 set,最初包含相同的元素(如果有的话)。
noneOf(Class<E> elementType) 创建一个具有指定元素类型的空枚举 set。
of(E e) 创建一个最初包含指定元素的枚举 set。重载函数可以接受多个元素
range(E from, E to) 创建一个最初包含由两个指定端点所定义范围内的所有元素的枚举 set。

EnumMap

所有键都必须来自单个 enum。

“命令模式”,就是把“命令”当做 Key 值,然后相应的“行动”当做 Value 值,存储到 EnumMap 中。

enum AlarmPoint {
    BATHROOM, UTILITY, KITCHEN
}

interface Command { void action(); }

public class EnumMaps {
    public static void main(String[] args) {
        EnumMap<AlarmPoint,Command> em = new EnumMap<AlarmPoint,Command>(AlarmPoint.class);
        em.put(AlarmPoint.KITCHEN, new Command() {
            public void action() { print("Kitchen fire!"); }
        });
        em.put(AlarmPoint.BATHROOM, new Command() {
            public void action() { print("Bathroom alert!"); }
        });
        for(Map.Entry<AlarmPoint,Command> e : em.entrySet()) {
            printnb(e.getKey() + ": ");
            e.getValue().action();
        }
    }
} /* Output:
BATHROOM: Bathroom alert!
KITCHEN: Kitchen fire!
*///:~

每个枚举实例有自己特有的行为方法,是实际开发中非常常见的一种抽象。这种场景下,用 EnumMap 能使代码很清晰,简洁。

常量相关的方法

Java 允许 enum 实例编写方法,从而为每个 enum 实例赋予各自不同的行为。你需要为 enum 定义一个或多个 abstract 方法,然后为每个 enum 实例实现该抽象方法。

通过相应的 enum 实例,我们可以调用其上的方法。这通常也称为表驱动的代码,注意它与命令模式的区别。

enum LikeClasses {
    WINKEN { void behavior() { print("Behavior1"); } },
    BLINKEN { void behavior() { print("Behavior2"); } },
    NOD { void behavior() { print("Behavior3"); } };
    abstract void behavior();
}

职责链(Chain of Responsibility)是很常用的一种抽象。它的本质是把一系列的“操作”抽象成解决问题的一系列方法。在遇到问题之后,进行一一尝试,直到问题被解决为止,或者最终被标记为不可解决。

多路分发

问题

多路分发(Multiple Dispatching)”模式源自于多对象交互的场景。比如下面这个算数的例子。

interface Number{
    public Number plus(Number n);
    public Number multiple(Number n);
}
class Integer extends Number{}
class Real extends Number{}
class Rational extends Number{}

Number 接口面向其他 Number 定义了加法和乘法。而 Number 底下有自然数,实数,有理数这样的派生类。所以当我们用两个数做加法或乘法的时候会像下面这样:

Number a=new Integer();
Number b=new Real();
a.plus(b);

但这里的问题是 Java 只支持单路分发,编译器只能对一个对象实施动态绑定。所以 a.plus(b); 是无法编译的。

解决方案

“石头剪刀布”游戏的例子

interface Item {
    Outcome compete(Item it);
    Outcome eval(Paper p);
    Outcome eval(Scissors s);
    Outcome eval(Rock r);
}

class Paper implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return DRAW; }
    public Outcome eval(Scissors s) { return WIN; }
    public Outcome eval(Rock r) { return LOSE; }
}
class Scissors implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return LOSE; }
    public Outcome eval(Scissors s) { return DRAW; }
    public Outcome eval(Rock r) { return WIN; }
}
class Rock implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return WIN; }
    public Outcome eval(Scissors s) { return LOSE; }
    public Outcome eval(Rock r) { return DRAW; }
}

通过调用 Item.compete() 方法开始两路分发。当系统动态绑定 it 对象的运行时类型后,再通过 it 对象回调 this 对象。

用枚举型多路分发

用枚举实现多路分发,完全是另外一个思路:“写死”。下面是书里给的例子:

public enum RoShamBo2 implements Competitor<RoShamBo2> {
    PAPER(DRAW, LOSE, WIN),
    SCISSORS(WIN, DRAW, LOSE),
    ROCK(LOSE, WIN, DRAW);
    private Outcome vPAPER, vSCISSORS, vROCK;
    RoShamBo2(Outcome paper,Outcome scissors,Outcome rock) {
        this.vPAPER = paper;
        this.vSCISSORS = scissors;
        this.vROCK = rock;
    }

    public Outcome compete(RoShamBo2 it) {
        switch(it) {
            default:
            case PAPER: return vPAPER;
            case SCISSORS: return vSCISSORS;
            case ROCK: return vROCK;
        }
    }
}

“原理”也很简单,用枚举把石头,剪刀,布之间的胜负关系全部写死。实际玩游戏的时候,对参数进行 switch。

使用常量相关的方法

public interface Competitor<T extends Competitor<T>> {
    Outcome compete(T competitor);
}
public enum RoShamBo4 implements Competitor<RoShamBo4> {
    ROCK {
        public Outcome compete(RoShamBo4 opponent) {
            return compete(SCISSORS, opponent);
        }
    },
    SCISSORS {
        public Outcome compete(RoShamBo4 opponent) {
            return compete(PAPER, opponent);
        }
    },
    PAPER {
        public Outcome compete(RoShamBo4 opponent) {
            return compete(ROCK, opponent);
        }
    };
    Outcome compete(RoShamBo4 loser, RoShamBo4 opponent) {
        return ((opponent == this) ? Outcome.DRAW
                : ((opponent == loser) ? Outcome.WIN : Outcome.LOSE));
    }
}

使用 EnumMap 分发

enum RoShamBo5 implements Competitor<RoShamBo5> {
    PAPER, SCISSORS, ROCK;
    static EnumMap<RoShamBo5, EnumMap<RoShamBo5, Outcome>> table =
                new EnumMap<RoShamBo5, EnumMap<RoShamBo5, Outcome>>(RoShamBo5.class);
    static {
        for (RoShamBo5 it : RoShamBo5.values()){
            table.put(it, new EnumMap<RoShamBo5, Outcome>(RoShamBo5.class));
        }
        initRow(PAPER, DRAW, LOSE, WIN);
        initRow(SCISSORS, WIN, DRAW, LOSE);
        initRow(ROCK, LOSE, WIN, DRAW);
    }
    static void initRow(RoShamBo5 it, Outcome vPAPER, Outcome vSCISSORS,Outcome vROCK) {
        EnumMap<RoShamBo5, Outcome> row = RoShamBo5.table.get(it);
        row.put(RoShamBo5.PAPER, vPAPER);
        row.put(RoShamBo5.SCISSORS, vSCISSORS);
        row.put(RoShamBo5.ROCK, vROCK);
    }
    public Outcome compete(RoShamBo5 it) {
        return table.get(this).get(it);
    }
}

使用二维数组

最简洁,最直接的方案。

enum RoShamBo6 implements Competitor<RoShamBo6> {
    PAPER, SCISSORS, ROCK;
    private static Outcome[][] table = {
        { DRAW, LOSE, WIN }, // PAPER
        { WIN, DRAW, LOSE }, // SCISSORS
        { LOSE, WIN, DRAW }, // ROCK
    };
    public Outcome compete(RoShamBo6 other) {
        return table[this.ordinal()][other.ordinal()];
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Java编程思想》笔记19-枚举类型

文章字数:2.3k

本文作者:Bin

发布时间:2018-06-23, 15:59:24

最后更新:2019-08-06, 00:46:42

原始链接:http://coolview.github.io/2018/06/23/Java%E7%BC%96%E7%A8%8B%E6%80%9D%E6%83%B3/%E3%80%8AJava%E7%BC%96%E7%A8%8B%E6%80%9D%E6%83%B3%E3%80%8B%E7%AC%94%E8%AE%B019-%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录