文章目录
Java中的枚举
22.1基本enum
特性
调用
enum
的values()
方法,可以遍历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)
时,其实并不知道a
或b
的确切类型,那么如何才能让它们正确地交互。
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
。由于目的是摸索出两种未知的类型,所以可以用一个EnumMap
的EnumMap
来实现两路分发:
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);
}
}
虽然简短,但表达能力却更强,部分原因是其代码更易于理解与修改,而且也更直接。不过,由于它使用的是数组,所以这种方式不太安全。如果使用一个大型数组,可能会不小心使用了错误的尺寸,而且,如果测试不能覆盖所有的可能性,有些错误可能会从眼前溜过。