22.Java中的枚举

Java中的枚举

22.1基本enum特性

调用enumvalues()方法,可以遍历enum实例。values()方法返回enum实例的数组,而且该数组中的元素严格保持其在enum中声明式的顺序,因此可以在循环中使用values()返回的数组。
创建enum时,编译器会生成一个相关的类,这个类继承自java.lang.Enum

// Capabilities of the Enum class
public class EnumClass {
    public static void main(String[] args) {
        for (Shrubbery s : Shrubbery.values()) {
            System.out.println(s + " ordinal: " + s.ordinal());
            System.out.print(s.compareTo(Shrubbery.CRAWLING) + " ");
            System.out.print(s.equals(Shrubbery.CRAWLING) + " ");
            System.out.println(s == Shrubbery.CRAWLING);
            System.out.println(s.getDeclaringClass());
            System.out.println(s.name());
            System.out.println("********************");
        }

        // Produce an enum value from a String name:
        for (String s : "HANGING CRAWLING GROUND".split(" ")) {
            Shrubbery shrub = Enum.valueOf(Shrubbery.class, s);
            System.out.println(shrub);
        }
    }
}

enum Shrubbery {GROUND, CRAWLING, HANGING}

ordinal()方法返回一个int值,这是每个enum实例在声明时的次序,从0开始,可以使用==来比较enum实例,编译器会自动提供equals()hashCode()方法。Enum类实现了Comparable接口,所以它具有compareTo方法。同时,它还实现了Serializable接口。
如果在enum实例上调用getDeclaringClass()方法,就能知道其所属的enum类。
name方法返回enum实例声明时的名字,这与使用toString()方法效果相同。valueOf()是在Enum中定义的static方法,它根据给定的名字返回相应的enum实例,如果不存在给定名字的实例,将会抛出异常。

22.2方法添加

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

// The witches in the land of Oz
public enum OzWitch {
    // Instances must be defined first, before methods:
    WEST("Miss Gulch, aka the Wicked Witch of the West"),
    NORTH("Glinda, the Good Witch of the North"),
    EAST("Wicked Witch of the East, wearer of the Ruby Slippers, crushed by Dorothy's house"),
    SOUTH("Good by inference, but missing");

    private String description;

    // Constructor must be package or private access:
    private OzWitch(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public static void main(String[] args) {
        for (OzWitch witch : OzWitch.values()) {
            System.out.println(witch + ": " + witch.getDescription());
        }
    }
}

如果在定义enum实例之前定义了任何方法或属性,那么在编译时就会得到错误信息。

22.3switch语句中的enum

虽然一般情况下必须使用enum类型来修饰一个enum实例,但是在case语句中不必如此。例如,构造一个小型状态机:

// Enums in switch statements
// Define an enum type
public class TrafficLight {
    Signal color = Signal.RED;

    public void change() {
        switch (color) {
            // Note you don't have to say Signal.RED in the case statement:
            case RED:
                color = Signal.GREEN;
                break;
            case GREEN:
                color = Signal.YELLOW;
                break;
            case YELLOW:
                color = Signal.RED;
                break;
        }
    }

    @Override
    public String toString() {
        return "The traffic light is " + color;
    }

    public static void main(String[] args) {
        TrafficLight t = new TrafficLight();

        for (int i = 0; i < 7; i++) {
            System.out.println(t);
            t.change();
        }
    }
}

enum Signal {GREEN, YELLOW, RED}
22.4values方法的神秘之处

编译器创建的enum类都继承自Enum类。然后,如果研究一下Enum类就会发现,它并没有values()方法。可以利用反射机制编写一个简单的程序,来查看其中的究竟:

public class Reflection {
    public static Set<String> analyze(Class<?> enumClass) {
        System.out.println("_____ Analyzing " + enumClass + " _____");
        System.out.println("Interfaces:");

        for (Type t : enumClass.getGenericInterfaces()) {
            System.out.println(t);
        }

        System.out.println("Base: " + enumClass.getSuperclass());
        System.out.println("Methods: ");

        Set<String> methods = new TreeSet<>();

        for (Method m : enumClass.getMethods()) {
            methods.add(m.getName());
        }

        System.out.println(methods);

        return methods;
    }

    public static void main(String[] args) {
        Set<String> exploreMethods = analyze(Explore.class);
        Set<String> enumMethods = analyze(Enum.class);

        System.out.println("Explore.containsAll(Enum)? " + exploreMethods.containsAll(enumMethods));
        System.out.print("Explore.removeAll(Enum): ");

        exploreMethods.removeAll(enumMethods);

        System.out.println(exploreMethods);
        // Decompile the code for the enum:
        OSExecute.command("javap -cp build/classes/main Explore");
    }
}

enum Explore {HERE, THERE}
// 输出为:
_____ Analyzing class Explore _____
Interfaces:
Base: class java.lang.Enum
Methods:
[compareTo, equals, getClass, getDeclaringClass,
hashCode, name, notify, notifyAll, ordinal, toString,
valueOf, values, wait]
_____ Analyzing class java.lang.Enum _____
Interfaces:
java.lang.Comparable<E>
interface java.io.Serializable
Base: class java.lang.Object
Methods:
[compareTo, equals, getClass, getDeclaringClass,
hashCode, name, notify, notifyAll, ordinal, toString,
valueOf, wait]
Explore.containsAll(Enum)? true
Explore.removeAll(Enum): [values]
Compiled from "Reflection.java"
final class Explore extends java.lang.Enum<Explore> {
    public static final Explore HERE;
    public static final Explore THERE;
    public static Explore[] values();
    public static Explore valueOf(java.lang.String);
    static {};
}

答案是,values()是由编译器添加的static方法。可以看出,在创建Explore的过程中,编译器还为其添加了valueOf()方法。
需要注意的是,虽然在Enum类中已经有valueOf()方法,但是其需要两个参数,而这个新增的方法只需要一个参数。
从最后的输出中可以看到,编译器将Explore标记为final类,所以无法继承自enum
由于擦除效应,反编译无法得到Enum的完整信息,所以它展示的Explore的父类只是一个原始的Enum,而非事实上的Enum<Explore>
由于values()方法是由编译器插入到enum定义中的static方法,所以,如果将enum实例向上转型为Enum,那么values()方法就不可访问了。不过,在Class中有一个getEnumConstants()方法,所以即便Enum接口中没有values()方法,仍然可以通过Class对象取得所有的enum实例。

// No values() method if you upcast an enum
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);
        }
    }
}

enum Search {HITHER, YON}

因为getEnumConstants()Class上的方法,所以甚至可以对不是枚举的类调用此方法。只不过,此时该方法返回null,所以当试图使用其返回的结果时会发生异常。

22.6随机选择

可以利用泛型,从enum实例中进行随机选择,从而使得这个工作更一般化:

public class Enums {
    private static Random rand = new Random(47);

    public static <T extends Enum<T>> T random(Class<T> ec) {
        return random(ec.getEnumConstants());
    }

    public static <T> T random(T[] values) {
        return values[rand.nextInt(values.length)];
    }
}

public class RandomTest {
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            System.out.print(Enums.random(Activity.class) + " ");
        }
    }
}

enum Activity {
    SITTING, LYING, STANDING, HOPPING, RUNNING, DODGING, 
    JUMPING, FALLING, FLYING
}
22.7使用接口组织枚举

无法从enum继承子类有时很令人沮丧。这种需求有时源自希望扩展原enum中的元素,有时是因为希望使用子类将一个enum中的元素进行分组。
在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组,可以达到将枚举元素分类组织的目的:

public interface Food {
    // 对于enum而言,实现接口是使其子类化的唯一方法
    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 class TypeOfFood {
    public static void main(String[] args) {
        Food food = Food.Appetizer.SALAD;
        food = Food.MainCourse.LASAGNE;
        food = Food.Dessert.GELATO;
        food = Food.Coffee.CAPPUCCINO;
    }
}

然而,当需要与一大堆类型打交道时,接口就不如enum好用了。

// 如果想创建一个枚举的枚举,那么可以创建一个新的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 class Meal {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            for (Course course : Course.values()) {
                Food food = course.randomSelection();
                System.out.println(food);
            }

            System.out.println("***");
        }
    }
}

此外,还有一种更简洁的管理枚举的方法,就是将一个enum嵌套在另一个enum内:

public enum SecurityCategory {
    STOCK(Security.Stock.class), BOND(Security.Bond.class);

    Security[] values;

    SecurityCategory(Class<? extends Security> kind) {
        values = kind.getEnumConstants();
    }

    // Security接口的作用是将其所包含的enum组合成一个公共类型
    interface Security {
        enum Stock implements Security {SHORT, LONG, MARGIN}

        enum Bond implements Security {MUNICIPAL, JUNK}
    }

    public Security randomSelection() {
        return Enums.random(values);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            SecurityCategory category = Enums.random(SecurityCategory.class);
            System.out.println(category + ": " + category.randomSelection());
        }
    }
}

// 如果将这种方式应用于Food的列子,结果应该是这样:
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<? 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 < 5; i++) {
            for (Meal2 meal : Meal2.values()) {
                Food food = meal.randomSelection();
                System.out.println(food);
            }

            System.out.println("***");
        }
    }
}
// 其实,这仅仅是重新组织了一下代码,不过多数情况下,这种方式使代码具有更清晰的结构
22.8使用EnumSet替代Flags

EnumSet中的元素必须来自一个enum

public class EnumSets {
    public static void main(String[] args) {
        EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class); // Empty
        points.add(AlarmPoints.BATHROOM);

        System.out.println(points);

        points.addAll(
            EnumSet.of(AlarmPoints.STAIR1, AlarmPoints.STAIR2, AlarmPoints.KITCHEN)
        );

        System.out.println(points);

        points = EnumSet.allOf(AlarmPoints.class);
        points.removeAll(EnumSet.of(AlarmPoints.STAIR1, AlarmPoints.STAIR2, AlarmPoints.KITCHEN));

        System.out.println(points);

        points.removeAll(EnumSet.range(AlarmPoints.OFFICE1, AlarmPoints.OFFICE4));

        System.out.println(points);

        points = EnumSet.complementOf(points);

        System.out.println(points);
    }
}

public enum AlarmPoints {
    STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
    OFFICE4, BATHROOM, UTILITY, KITCHEN
}
22.9使用EnumMap

EnumMap是一种特殊的Map,它要求其中的键必须来自一个enum,由于enum本身的限制,所以EnumMap在内部可由数组实现。因此EnumMap的速度很快,可以放心地使用enum实例在EnumMap进行查找操作。不过,只能将enum的实例作为键来调用put方法,其他的操作与使用一般的Map差不多。

/**
 * 使用命令设计模式,首先需要一个只有单一方法的接口,然后从该接口实现具有各自不同的行为的多个子类,
 * 接下来,就可以构造命令对象,并在需要的时候使用它们了
 */
public class EnumMaps {
    public static void main(String[] args) {
        EnumMap<AlarmPoints, Command> em = new EnumMap<>(AlarmPoints.class);
        em.put(AlarmPoints.KITCHEN, () -> System.out.println("Kitchen fire!"));
        em.put(AlarmPoints.BATHROOM, () -> System.out.println("Bathroom alert!"));

        for (Map.Entry<AlarmPoints, Command> e : em.entrySet()) {
            System.out.print(e.getKey() + ": ");
            e.getValue().action();
        }

        try { // If there's no value for a particular key:
            em.get(AlarmPoints.UTILITY).action();
        } catch (Exception e) {
            System.out.println("Expected: " + e);
        }
    }
}

interface Command {
    void action();
}

EnumSet一样,enum实例定义时的次序决定了其在EnumMap中的顺序
enum的每个实例作为一个键,总是存在的。但是,如果没有为这个键调用put()方法来存入相应的值的话,其对应的值就是null
与常量相关的方法相比,EnumMap有一个优点,那就是允许改变值对象,而常量相关的方法在编译期就被固定了。并且,当有多种类型的enum而且它们之间存在相互操作的情况下,可以用EnumMap实现多路分发。

22.10常量特定方法

Java的enum有一个非常有趣的特性,即它允许为enum实例编写方法,从而为每个enum实例赋予各自不同的行为。要实现常量相关的方法,需要为enum定义一个或多个abstract方法,然后为每个enum实例实现该抽象方法:

public enum ConstantSpecificMethod {
    DATE_TIME {
        @Override
        String getInfo() {
            return DateFormat.getDateInstance().format(new Date());
        }

    },
    CLASSPATH {
        @Override
        String getInfo() {
            return System.getenv("CLASSPATH");
        }
    },
    VERSION {
        @Override
        String getInfo() {
            return System.getProperty("java.version");
        }
    };

    abstract String getInfo();

    public static void main(String[] args) {
        for (ConstantSpecificMethod csm : values()) {
            System.out.println(csm.getInfo());
        }
    }
}

通过相应的enum实例,可以调用其上的方法。这通常也称为表驱动(table-driven code)的代码。
在面向对象的程序设计中,不同的行为与不同的类关联。而通过常量相关的方法,每个enum实例可以具备自己独特的行为。然而,并不能真的将enum实例作为一个类型来使用:

public class NotClasses {
    // void f1(LikeClasses.WINKEN instance) {} // Nope
}

// {javap -c LikeClasses}
enum LikeClasses {
    WINKEN {
        @Override
        void behavior() {
            System.out.println("Behavior1");
        }
    },
    BLINKEN {
        @Override
        void behavior() {
            System.out.println("Behavior2");
        }
    },
    NOD {
        @Override
        void behavior() {
            System.out.println("Behavior3");
        }
    };

    abstract void behavior();
}

编译器不允许将一个enum实例当作class类型。如果分析一下编译器生成的代码,就知道这种行为也是很正常的。因为每个enum元素都是一个LikeClasses类型的static final实例。
同时,由于它们是static实例,无法访问外部类的非static元素或方法,所以对于内部的enum实例而言,其行为与一般的内部类并不相同。

public class CarWash {
    public enum Cycle {
        UNDERBODY {
            @Override
            void action() {
                System.out.println("Spraying the underbody");
            }

        }, WHEELWASH {
            @Override
            void action() {
                System.out.println("Washing the wheels");
            }
        }, PREWASH {
            @Override
            void action() {
                System.out.println("Loosening the dirt");
            }
        }, BASIC {
            @Override
            void action() {
                System.out.println("The basic wash");
            }
        }, HOTWAX {
            @Override
            void action() {
                System.out.println("Applying hot wax");
            }
        }, RINSE {
            @Override
            void action() {
                System.out.println("Rinsing");
            }
        }, BLOWDRY {
            @Override
            void action() {
                System.out.println("Blowing dry");
            }
        };

        abstract void action();
    }

    EnumSet<Cycle> cycles = EnumSet.of(Cycle.BASIC, Cycle.RINSE);

    public void add(Cycle cycle) {
        cycles.add(cycle);
    }

    public void washCar() {
        for (Cycle c : cycles) {
            c.action();
        }
    }

    @Override
    public String toString() {
        return cycles.toString();
    }

    public static void main(String[] args) {
        CarWash wash = new CarWash();
        System.out.println(wash);
        wash.washCar();

        // Order of addition is unimportant:
        wash.add(Cycle.BLOWDRY);
        wash.add(Cycle.BLOWDRY); // Duplicates ignored
        wash.add(Cycle.RINSE);
        wash.add(Cycle.HOTWAX);

        System.out.println(wash);

        wash.washCar();
    }
}

与使用匿名内部类相比较,定义常量相关的方法的语法更高效、简洁。
除了实现abstract方法以外,也可以覆盖常量相关的方法:

public enum OverrideConstantSpecific {
    NUT, BOLT, WASHER {
        @Override
        void f() {
            System.out.println("Overridden method");
        }
    };

    // 不是abstract
    void f() {
        System.out.println("default behavior");
    }

    public static void main(String[] args) {
        for (OverrideConstantSpecific ocs : values()) {
            System.out.print(ocs + ": ");
            ocs.f();
        }
    }
}

虽然enum有某些限制,但是一般而言,还是可以将其看作是类。

22.10.1使用enum的职责链

在职责链设计模式中,以多种不同的方式来解决一个问题,然后将它们链接在一起。当一个请求到来时,它遍历这个链,直到链中的某个解决方案能够处理该请求。
通过常量相关的方法,可以很容易地实现一个简单的职责链。

// Modeling a post office
public class PostOffice {
    enum MailHandler {
        GENERAL_DELIVERY {
            @Override
            boolean handle(Mail m) {
                switch (m.generalDelivery) {
                    case YES:
                        System.out.println("Using general delivery for " + m);
                        return true;
                    default:
                        return false;
                }
            }
        }, MACHINE_SCAN {
            @Override
            boolean handle(Mail m) {
                switch (m.scannability) {
                    case UNSCANNABLE:
                        return false;
                    default:
                        switch (m.address) {
                            case INCORRECT:
                                return false;
                            default:
                                System.out.println("Delivering " + m + " automatically");
                                return true;
                        }
                }
            }
        }, VISUAL_INSPECTION {
            @Override
            boolean handle(Mail m) {
                switch (m.readability) {
                    case ILLEGIBLE:
                        return false;
                    default:
                        switch (m.address) {
                            case INCORRECT:
                                return false;
                            default:
                                System.out.println("Delivering " + m + " normally");
                                return true;
                        }
                }
            }
        }, RETURN_TO_SENDER {
            @Override
            boolean handle(Mail m) {
                switch (m.returnAddress) {
                    case MISSING:
                        return false;
                    default:
                        System.out.println("Returning " + m + " to sender");
                        return true;
                }
            }
        };

        abstract boolean handle(Mail m);
    }

    static void handle(Mail m) {
        for (MailHandler handler : MailHandler.values()) {
            if (handler.handle(m)) {
                return;
            }
        }

        System.out.println(m + " is a dead letter");
    }

    public static void main(String[] args) {
        for (Mail mail : Mail.generator(10)) {
            System.out.println(mail.details());
            handle(mail);
            System.out.println("*****");
        }
    }
}

class Mail {
    // The NO's reduce probability of random selection:
    enum GeneralDelivery {YES, NO1, NO2, NO3, NO4, NO5}

    enum Scannability {UNSCANNABLE, YES1, YES2, YES3, YES4}

    enum Readability {ILLEGIBLE, YES1, YES2, YES3, YES4}

    enum Address {INCORRECT, OK1, OK2, OK3, OK4, OK5, OK6}

    enum ReturnAddress {MISSING, OK1, OK2, OK3, OK4, OK5}

    GeneralDelivery generalDelivery;
    Scannability scannability;
    Readability readability;
    Address address;
    ReturnAddress returnAddress;

    static long counter = 0;
    long id = counter++;

    @Override
    public String toString() {
        return "Mail " + id;
    }

    public String details() {
        return toString() + ", General Delivery: " + generalDelivery + ", Address Scanability: "
                + scannability + ", Address Readability: " + readability + ", Address Address: "
                + address + ", Return address: " + returnAddress;

    }

    // Generate test Mail:
    public static Mail randomMail() {
        Mail m = new Mail();
        m.generalDelivery = Enums.random(GeneralDelivery.class);
        m.scannability = Enums.random(Scannability.class);
        m.readability = Enums.random(Readability.class);
        m.address = Enums.random(Address.class);
        m.returnAddress = Enums.random(ReturnAddress.class);
        return m;
    }

    public static Iterable<Mail> generator(final int count) {
        return new Iterable<Mail>() {
            int n = count;

            @Override
            public Iterator<Mail> iterator() {
                return new Iterator<Mail>() {
                    @Override
                    public boolean hasNext() {
                        return n-- > 0;
                    }

                    @Override
                    public Mail next() {
                        return randomMail();
                    }

                    @Override
                    public void remove() { // Not implemented
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }
}
22.10.2使用enum的状态机

枚举类型非常适合用来创建状态机。一个状态机可以具有有限个特定的状态,它通常根据输入,从一个状态转移到下一个状态,不过也可能存在瞬时状态,而一旦任务结束,状态机就会立刻离开瞬时状态。
每个状态都具有某些可接受的输入,不同的输入会使状态机从当前状态转移到不同的新状态。由于enum对其实例有严格限制,非常适合用来表现不同的状态和输入。一般而言,每个状态都具有一些相关的输出。

// 自动售货机是一个很好的状态机的例子
public enum Input {
    // 注意,除了两个特殊的Input实例外,其他的Input都有相应的价格,因此在接口中定义了amount方法。
    // 然而,对那两个特殊的Input实例而言,调用amount方法并不合适,所以如果调用就会有异常抛出,
    // 这似乎有点奇怪,但由于enum的限制,不得不采用这种方式
    NICKEL(5), DIME(10), QUARTER(25), DOLLAR(100), TOOTHPASTE(200), CHIPS(75), 
    SODA(100), SOAP(50),ABORT_TRANSACTION {
        @Override
        public int amount() { // Disallow
            throw new RuntimeException("ABORT.amount()");
        }
    }, STOP { // This must be the last instance.
        @Override
        public int amount() { // Disallow
            throw new RuntimeException("SHUT_DOWN.amount()");
        }
    };

    int value; // In cents

    Input(int value) {
        this.value = value;
    }

    Input() {
    }

    // In cents
    int amount() {
        return value;
    }

    static Random rand = new Random(47);

    public static Input randomSelection() {
        // Don't include STOP(包头不包尾):
        return values()[rand.nextInt(values().length - 1)];
    }
}

// {java VendingMachine VendingMachineInput.txt}
public class VendingMachine {
    private static State state = State.RESTING;
    private static int amount = 0;
    private static Input selection = null;

    enum StateDuration {TRANSIENT} // Tagging enum

    enum State {
        RESTING {
            @Override
            void next(Input input) {
                switch (Category.categorize(input)) {
                    case MONEY:
                        amount += input.amount();
                        state = ADDING_MONEY;
                        break;
                    case SHUT_DOWN:
                        state = TERMINAL;
                    default:
                }
            }
        }, ADDING_MONEY {
            @Override
            void next(Input input) {
                switch (Category.categorize(input)) {
                    case MONEY:
                        amount += input.amount();
                        break;
                    case ITEM_SELECTION:
                        selection = input;
                        if (amount < selection.amount()) System.out.println("Insufficient money for " + selection);
                        else state = DISPENSING;
                        break;
                    case QUIT_TRANSACTION:
                        state = GIVING_CHANGE;
                        break;
                    case SHUT_DOWN:
                        state = TERMINAL;
                    default:
                }
            }
        }, DISPENSING(StateDuration.TRANSIENT) {
            @Override
            void next() {
                System.out.println("here is your " + selection);

                amount -= selection.amount();
                state = GIVING_CHANGE;
            }
        }, GIVING_CHANGE(StateDuration.TRANSIENT) {
            @Override
            void next() {
                if (amount > 0) {
                    System.out.println("Your change: " + amount);
                    amount = 0;
                }

                state = RESTING;
            }
        }, TERMINAL {
            @Override
            void output() {
                System.out.println("Halted");
            }
        };

        private boolean isTransient = false;

        State() {
        }

        State(StateDuration trans) {
            isTransient = true;
        }

        void next(Input input) {
            throw new RuntimeException("Only call " + "next(Input input) for non-transient states");
        }

        void next() {
            throw new RuntimeException("Only call next() for " + "StateDuration.TRANSIENT states");
        }

        void output() {
            System.out.println(amount);
        }
    }

    static void run(Supplier<Input> gen) {
        while (state != State.TERMINAL) {
            state.next(gen.get());

            while (state.isTransient) {
                state.next();
            }

            state.output();
        }
    }

    public static void main(String[] args) {
        Supplier<Input> gen = new RandomInputSupplier();

        if (args.length == 1) {
            gen = new FileInputSupplier(args[0]);
        }

        run(gen);
    }
}

enum Category {
    // 反编译之后这些实例其实是static final的,因此会在static初始化块之前执行
    MONEY(Input.NICKEL, Input.DIME, Input.QUARTER, Input.DOLLAR),
    ITEM_SELECTION(Input.TOOTHPASTE, Input.CHIPS, Input.SODA, Input.SOAP),
    QUIT_TRANSACTION(Input.ABORT_TRANSACTION), SHUT_DOWN(Input.STOP);

    private Input[] values;

    Category(Input... types) {
        values = types;
    }

    private static EnumMap<Input, Category> categories = new EnumMap<>(Input.class);

    static {
        for (Category c : Category.class.getEnumConstants()) {
            for (Input type : c.values) {
                categories.put(type, c);
            }
        }
    }

    public static Category categorize(Input input) {
        return categories.get(input);
    }
}

// For a basic sanity check:
class RandomInputSupplier implements Supplier<Input> {
    @Override
    public Input get() {
        return Input.randomSelection();
    }
}

// Create Inputs from a file of ';'-separated strings:
class FileInputSupplier implements Supplier<Input> {
    private Iterator<String> input;

    // 将文件转换为流,并跳过注释行
    FileInputSupplier(String fileName) {
        try {
            input = Files.lines(Paths.get(fileName))
                    .skip(1) // Skip the comment line
                    .flatMap(s -> Arrays.stream(s.split(";")))
                    .map(String::trim)
                    .collect(Collectors.toList())
                    .iterator();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Input get() {
        if (!input.hasNext()) {
            return null;
        }

        return Enum.valueOf(Input.class, input.next().trim());
    }
}
// 这种设计有一个缺陷,它要求enum State实例访问的VendingMachine属性必须声明为static,
// 这意味着,只能有一个VendingMachine实例。不过如果仔细思考一下实际的(嵌入式java)应用,
// 这也许并不是一个大问题,因为在一台机器上,可能只有一个应用程序

// VendingMachineInput.txt文本内容
// enums/VendingMachineInput.txt
QUARTER; QUARTER; QUARTER; CHIPS; 
DOLLAR; DOLLAR; TOOTHPASTE; 
QUARTER; DIME; ABORT_TRANSACTION; 
QUARTER; DIME; SODA; 
QUARTER; DIME; NICKEL; SODA; 
ABORT_TRANSACTION; 
STOP;
22.11多路分发

当要处理多种交互类型时,程序可能会变得相当杂乱。举例来说,如果一个系统要分析和执行数学表达式,可能会声明Number.plus(Number)Number.multiple(Number)等等,其中Number是各种数字对象的超类。然而,当声明a.plus(b)时,其实并不知道ab的确切类型,那么如何才能让它们正确地交互。
Java只支持单路分发。也就是说,如果要执行的操作包含了不止一个类型未知的对象时,那么java的动态绑定机制只能处理其中的一个类型。这就无法解决上面提到的问题。所以,必须自己来判定其他的类型,从而实现自己的动态绑定行为。
解决上面问题的办法就是多路分发(在那个例子中,只有两个分发,一般称之为两路分发)。多态只能发生在方法调用时,所以,如果想使用两路分发,那么就必须有两个方法调用:第一个方法调用决定第一个未知类型,第二个方法调用决定第二个未知的类型。要利用多路分发,必须为每一个类型提供一个实际的方法调用,如果要处理两个不同的类型体系,就需要为每个类型体系执行一个方法调用。一般而言,需要有设定好的某种配置,以便一个方法调用能够引出更多的方法调用,从而能够在这个过程中处理多种类型。为了达到这种效果,需要与多个方法一同工作:因为每个分发都需要一个方法调用。

// 在下面的例子中实现了“石头、剪刀、布”游戏,也称为 RoShamBo,对应的方法是compete()和eval(),
// 二者都是同一个类型的成员,它们可以产生三种Outcome实例中的一个作为结果:
public enum Outcome { WIN, LOSE, DRAW }

public class RoShamBo1 {
    static final int SIZE = 20;
    private static Random rand = new Random(47);

    public static Item newItem() {
        switch (rand.nextInt(3)) {
            default:
            case 0:
                return new Scissors();
            case 1:
                return new Paper();
            case 2:
                return new Rock();
        }
    }

    /**
     * RoShamBo1.match()有两个Item参数,通过调用Item.compete()方法开始两路分发。
     * 要判定a的类型,分发机制会在a的实际类型的compete()内部起到分发的作用。
     * compete()方法通过调用eval()来为另一个类型实现第二次分发。将自身(this)作为参数
     * 调用eval(),能够调用重载过的eval()方法,这能够保留第一次分发的类型信息。
     * 当第二次分发完成时,就能够知道两个Item对象的具体类型了
     */
    public static void match(Item a, Item b) {
        System.out.println(a + " vs. " + b + ": " + a.compete(b));
    }

    public static void main(String[] args) {
        for (int i = 0; i < SIZE; i++) {
            match(newItem(), newItem());
        }
    }
}

// Item是这几种类型的接口,将会被用作多路分发
interface Item {
    Outcome compete(Item it);

    Outcome eval(Paper p);

    Outcome eval(Scissors s);

    Outcome eval(Rock r);
}

class Paper implements Item {
    @Override
    public Outcome compete(Item it) {
        return it.eval(this);
    }

    @Override
    public Outcome eval(Paper p) {
        return Outcome.DRAW;
    }

    @Override
    public Outcome eval(Scissors s) {
        return Outcome.WIN;
    }

    @Override
    public Outcome eval(Rock r) {
        return Outcome.LOSE;
    }

    @Override
    public String toString() {
        return "Paper";
    }
}

class Scissors implements Item {
    @Override
    public Outcome compete(Item it) {
        return it.eval(this);
    }

    @Override
    public Outcome eval(Paper p) {
        return Outcome.LOSE;
    }

    @Override
    public Outcome eval(Scissors s) {
        return Outcome.DRAW;
    }

    @Override
    public Outcome eval(Rock r) {
        return Outcome.WIN;
    }

    @Override
    public String toString() {
        return "Scissors";
    }
}

class Rock implements Item {
    @Override
    public Outcome compete(Item it) {
        return it.eval(this);
    }

    @Override
    public Outcome eval(Paper p) {
        return Outcome.WIN;
    }

    @Override
    public Outcome eval(Scissors s) {
        return Outcome.LOSE;
    }

    @Override
    public Outcome eval(Rock r) {
        return Outcome.DRAW;
    }

    @Override
    public String toString() {
        return "Rock";
    }
}

要配置好多路分发需要很多的工序,不过要记住,它的好处在于方法调用时的优雅语法,这避免了在一个方法中判定多个对象的类型的丑陋代码。不过,在使用多路分发前,请先明确,这种优雅的代码确实有重要的意义。

22.11.1使用enum分发

直接将RoShamBo1.java翻译为基于enum的版本是有问题的,因为enum实例不是类型,不能将enum实例作为参数的类型,所以无法重载eval()分发。不过,还有很多方式可以实现多路分发,并从enum中获益。
一种方式是使用构造器来初始化每个enum实例,并以一组结果作为参数。这二者放在一块,形成了类似查询表的结构:

// Competitor接口定义了一种类型,该类型的对象可以与另一个Competitor相竞争
public interface Competitor<T extends Competitor<T>> {
    Outcome compete(T competitor);
}

public class RoShamBo {
    public static <T extends Competitor<T>> void match(T a, T b) {
        System.out.println(a + " vs. " + b + ": " + a.compete(b));
    }

    /**
     * 类型参数必须同时是Enum<T>类型(因为它将在Enums.random()中使用)
     * 和Competor<T>类型(因为它将被传递给match()方法)
     */
    public static <T extends Enum<T> & Competitor<T>> void play(
        Class<T> rsbClass, int size) {
        for (int i = 0; i < size; i++) {
            match(Enums.random(rsbClass), Enums.random(rsbClass));
        }
    }
}

public enum RoShamBo2 implements Competitor<RoShamBo2> {
    PAPER(Outcome.DRAW, Outcome.LOSE, Outcome.WIN), 
    SCISSORS(Outcome.WIN, Outcome.DRAW, Outcome.LOSE),
    ROCK(Outcome.LOSE, Outcome.WIN, Outcome.DRAW);

    private Outcome vPAPER, vSCISSORS, vROCK;

    RoShamBo2(Outcome paper, Outcome scissors, Outcome rock) {
        this.vPAPER = paper;
        this.vSCISSORS = scissors;
        this.vROCK = rock;
    }

    /**
     * 在compete()方法中,一旦两种类型都被确定了,那么唯一的操作就是返回结果Outcome。然而,
     * 可能还需要调用其他的方法,(例如)甚至是调用在构造器中指定的某个命令对象上的方法。
     * 仍然是使用两路分发来判定两个对象的类型,不过只有第一次分发是实际的方法调用,第二个分发
     * 使用的是switch,这样是安全的,因为enum限制了switch语句的选择分支
     */
    @Override
    public Outcome compete(RoShamBo2 it) {
        switch (it) {
            default:
            case PAPER:
                return vPAPER;
            case SCISSORS:
                return vSCISSORS;
            case ROCK:
                return vROCK;
        }
    }

    public static void main(String[] args) {
        RoShamBo.play(RoShamBo2.class, 20);
    }
}
22.11.2使用常量相关的方法

常量相关的方法允许为每个enum实例提供方法的不同实现,这使得常量相关的方法似乎是实现多路分发的完美解决方案。不过,通过这种方式,enum实例虽然可以具有不同的行为,但它们仍然不是类型,不能将其作为方法签名中的参数类型来使用。最好的办法是将enum用在switch语句中:

public enum RoShamBo3 implements Competitor<RoShamBo3> {
    PAPER {
        @Override
        public Outcome compete(RoShamBo3 it) {
            switch (it) {
                default: // To placate the compiler
                case PAPER:
                    return Outcome.DRAW;
                case SCISSORS:
                    return Outcome.LOSE;
                case ROCK:
                    return Outcome.WIN;
            }
        }
    }, SCISSORS {
        @Override
        public Outcome compete(RoShamBo3 it) {
            switch (it) {
                default:
                case PAPER:
                    return Outcome.WIN;
                case SCISSORS:
                    return Outcome.DRAW;
                case ROCK:
                    return Outcome.LOSE;
            }
        }
    }, ROCK {
        @Override
        public Outcome compete(RoShamBo3 it) {
            switch (it) {
                default:
                case PAPER:
                    return Outcome.LOSE;
                case SCISSORS:
                    return Outcome.WIN;
                case ROCK:
                    return Outcome.DRAW;
            }
        }
    };

    @Override
    public abstract Outcome compete(RoShamBo3 it);

    public static void main(String[] args) {
        RoShamBo.play(RoShamBo3.class, 20);
    }
}

// 虽然这种方式可以工作,但是却不甚合理,如果采用RoShamBo2的解决方案,那么在添加一个新的类型时,
// 只需要更少的代码,而且也更直接。
// 然而,RoShamBo3还可以压缩简化一下:
public enum RoShamBo4 implements Competitor<RoShamBo4> {
    ROCK {
        @Override
        public Outcome compete(RoShamBo4 opponent) {
            return compete(SCISSORS, opponent);
        }
    }, SCISSORS {
        @Override
        public Outcome compete(RoShamBo4 opponent) {
            return compete(PAPER, opponent);
        }
    }, PAPER {
        @Override
        public Outcome compete(RoShamBo4 opponent) {
            return compete(ROCK, opponent);
        }
    };

    /**
     * 具有两个参数的compete方法执行第二个分发,该方法执行一系列的比较,其行为类似switch语句。
     * 这个版本的程序更简短,不过却比较难理解。对于一个大型系统而言,难以理解的代码将导致整个
     * 系统不够健壮
     */
    Outcome compete(RoShamBo4 loser, RoShamBo4 opponent) {
        return ((opponent == this) ? Outcome.DRAW : ((opponent == loser) ? Outcome.WIN : Outcome.LOSE));
    }

    public static void main(String[] args) {
        RoShamBo.play(RoShamBo4.class, 20);
    }
}
22.11.3使用EnumMap分发

使用EnumMap能够实现真正的两路分发。EnumMap是为enum专门设计的一种性能非常好的特殊Map。由于目的是摸索出两种未知的类型,所以可以用一个EnumMapEnumMap来实现两路分发:

public enum RoShamBo5 implements Competitor<RoShamBo5> {
    PAPER, SCISSORS, ROCK;
    static EnumMap<RoShamBo5, EnumMap<RoShamBo5, Outcome>> table = 
        new EnumMap<>(RoShamBo5.class);

    static {
        for (RoShamBo5 it : RoShamBo5.values()) {
            table.put(it, new EnumMap<>(RoShamBo5.class));
        }

        initRow(PAPER, Outcome.DRAW, Outcome.LOSE, Outcome.WIN);
        initRow(SCISSORS, Outcome.WIN, Outcome.DRAW, Outcome.LOSE);
        initRow(ROCK, Outcome.LOSE, Outcome.WIN, Outcome.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);
    }

    // 在一行语句中发生了两次分发
    @Override
    public Outcome compete(RoShamBo5 it) {
        return table.get(this).get(it);
    }

    public static void main(String[] args) {
        RoShamBo.play(RoShamBo5.class, 20);
    }
}
22.11.4使用二维数组

还可以进一步简化实现两路分发的解决方案。可以看到,每个enum实例都有一个固定的值(基于其声明的次序),并且可以通过ordinal()方法取得该值。因此可以使用二维数组。采用这种方式能够获得最简洁、最直接的解决方案。

public enum RoShamBo6 implements Competitor<RoShamBo6> {
    PAPER, SCISSORS, ROCK;
    private static Outcome[][] table = {
            { Outcome.DRAW, Outcome.LOSE, Outcome.WIN }, // PAPER
            { Outcome.WIN, Outcome.DRAW, Outcome.LOSE }, // SCISSORS
            { Outcome.LOSE, Outcome.WIN, Outcome.DRAW }, // ROCK
    };

    @Override
    public Outcome compete(RoShamBo6 other) {
        return table[this.ordinal()][other.ordinal()];
    }

    public static void main(String[] args) {
        RoShamBo.play(RoShamBo6.class, 20);
    }
}

虽然简短,但表达能力却更强,部分原因是其代码更易于理解与修改,而且也更直接。不过,由于它使用的是数组,所以这种方式不太安全。如果使用一个大型数组,可能会不小心使用了错误的尺寸,而且,如果测试不能覆盖所有的可能性,有些错误可能会从眼前溜过。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值