From f86413690f3a6ab26240b726a9912c17c23fda99 Mon Sep 17 00:00:00 2001 From: sjsdfg <736777445@qq.com> Date: Sat, 10 Aug 2019 23:14:59 +0800 Subject: [PATCH] update --- docs/book/22-Enumerations.md | 1561 +++++++++++++++++++++++++++++++++- 1 file changed, 1556 insertions(+), 5 deletions(-) diff --git a/docs/book/22-Enumerations.md b/docs/book/22-Enumerations.md index cb6a29f..309d9cf 100644 --- a/docs/book/22-Enumerations.md +++ b/docs/book/22-Enumerations.md @@ -1,6 +1,7 @@ [TOC] + # 第二十二章 枚举 > 关键字 enum 可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用。这是一种非常有用的功能 @@ -203,8 +204,8 @@ toString() 方法通过调用 name() 方法取得 SpaceShip 的名字,然后 - + ## switch 语句中的 enum 在 switch 中使用 enum,是 enum 提供的一项非常便利的功能。一般来说,在 switch 中只能使用整数值,而枚举实例天生就具备整数值的次序,并且可以通过 ordinal() 方法取得其次序(显然编译器帮我们做了类似的工作),因此我们可以在 switch 语句中使用 enum。 @@ -261,8 +262,8 @@ The traffic light is RED - + ## values 方法的神秘之处 前面已经提到,编译器为你创建的 enum 类都继承自 Enum 类。然而,如果你研究一下 Enum 类就会发现,它并没有 values() 方法。可我们明明已经用过该方法了,难道存在某种“隐藏的”方法吗?我们可以利用反射机制编写一个简单的程序,来查看其中的究竟: @@ -396,8 +397,8 @@ Expected: java.lang.NullPointerException 只不过,此时该方法返回 null,所以当你试图使用其返回的结果时会发生异常。 - + ## 实现而非继承 我们已经知道,所有的 enum 都继承自 Java.lang.Enum 类。由于 Java 不支持多重继承,所以你的 enum 不能再继承其他类: @@ -500,8 +501,8 @@ STANDING LYING FALLING RUNNING FLYING LYING - + ## 使用接口组织枚举 无法从 enum 继承子类有时很令人沮丧。这种需求有时源自我们希望扩展原 enum 中的元素,有时是因为我们希望使用子类将一个 enum 中的元素进行分组。 @@ -531,28 +532,1578 @@ public interface Food { } ``` +对于 enum 而言,实现接口是使其子类化的唯一办法,所以嵌入在 Food 中的每个 enum 都实现了 Food 接口。现在,在下面的程序中,我们可以说“所有东西都是某种类型的 Food"。 +```java +// enums/menu/TypeOfFood.java +// {java enums.menu.TypeOfFood} +package enums.menu; +import static enums.menu.Food.*; +public class TypeOfFood { + public static void main(String[] args) { + Food food = Appetizer.SALAD; + food = MainCourse.LASAGNE; + food = Dessert.GELATO; + food = Coffee.CAPPUCCINO; + } +} +``` +如果 enum 类型实现了 Food 接口,那么我们就可以将其实例向上转型为 Food,所以上例中的所有东西都是 Food。 + +然而,当你需要与一大堆类型打交道时,接口就不如 enum 好用了。例如,如果你想创建一个“校举的枚举”,那么可以创建一个新的 enum,然后用其实例包装 Food 中的每一个 enum 类: + +```java +// enums/menu/Course.java +package enums.menu; +import onjava.*; +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 kind) { + values = kind.getEnumConstants(); + } + public Food randomSelection() { + return Enums.random(values); + } +} +``` + +每一个 Course 的实例都将其对应的 Class 对象作为构造器的参数。通过 getEnumConstants0 方法,可以从该 Class 对象中取得某个 Food 子类的所有 enum 实例。这些实例在 randomSelection() 中被用到。因此,通过从每一个 Course 实例中随机地选择一个 Food,我们便能够生成一份菜单: + +```java +// enums/menu/Meal.java +// {java enums.menu.Meal} +package enums.menu; +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("***"); + } + } +} +``` + +输出为: + +``` +SPRING_ROLLS +VINDALOO +FRUIT +DECAF_COFFEE +*** +SOUP +VINDALOO +FRUIT +TEA +*** +SALAD +BURRITO +FRUIT +TEA +*** +SALAD +BURRITO +CREME_CARAMEL +LATTE +*** +SOUP +BURRITO +TIRAMISU +ESPRESSO +*** +``` + +在这个例子中,我们通过遍历每一个 Course 实例来获得“枚举的枚举”的值。稍后,在 VendingMachine.java 中,我们会看到另一种组织枚举实例的方式,但其也有一些其他的限制。 + +此外,还有一种更简洁的管理枚举的办法,就是将一个 enum 嵌套在另一个 enum 内。就像这样: + +```java +// enums/SecurityCategory.java +// More succinct subcategorization of enums +import onjava.*; +enum SecurityCategory { + STOCK(Security.Stock.class), + BOND(Security.Bond.class); + Security[] values; + SecurityCategory(Class kind) { + values = kind.getEnumConstants(); + } + 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()); + } + } +} +``` + +输出为: + +``` +BOND: MUNICIPAL +BOND: MUNICIPAL +STOCK: MARGIN +STOCK: MARGIN +BOND: JUNK +STOCK: SHORT +STOCK: LONG +STOCK: LONG +BOND: MUNICIPAL +BOND: JUNK +``` + +Security 接口的作用是将其所包含的 enum 组合成一个公共类型,这一点是有必要的。然后,SecurityCategory 才能将 Security 中的 enum 作为其构造器的参数使用,以起到组织的效果。 + +如果我们将这种方式应用于 Food 的例子,结果应该这样: + +```java +// enums/menu/Meal2.java +// {java enums.menu.Meal2} +package enums.menu; +import onjava.*; +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 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("***"); + } + } +} +``` + +输出为: + +``` +SPRING_ROLLS +VINDALOO +FRUIT +DECAF_COFFEE +*** +SOUP +VINDALOO +FRUIT +TEA +*** +SALAD +BURRITO +FRUIT +TEA +*** +SALAD +BURRITO +CREME_CARAMEL +LATTE +*** +SOUP +BURRITO +TIRAMISU +ESPRESSO +*** +``` + +其实,这仅仅是重新组织了一下代码,不过多数情况下,这种方式使你的代码具有更清晰的结构。 + ## 使用 EnumSet 替代 Flags +Set 是一种集合,只能向其中添加不重复的对象。当然,enum 也要求其成员都是唯一的,所以 enumi 看起来也具有集合的行为。不过,由于不能从 enum 中删除或添加元素,所以它只能算是不太有用的集合。Java SE5 引入 EnumSet,是为了通过 enum 创建一种替代品,以替代传统的基于 int 的“位标志”。这种标志可以用来表示某种“开/关”信息,不过,使用这种标志,我们最终操作的只是一些 bit,而不是这些 bit 想要表达的概念,因此很容易写出令人难以理解的代码。 + +EnumSet 的设计充分考虑到了速度因素,因为它必须与非常高效的 bit 标志相竞争(其操作与 HashSet 相比,非常地快),就其内部而言,它(可能)就是将一个 long 值作为比特向量,所以 EnumSet 非常快速高效。使用 EnumSet 的优点是,它在说明一个二进制位是否存在时,具有更好的表达能力,并且无需担心性能。 + +EnumSet 中的元素必须来自一个 enum。下面的 enum 表示在一座大楼中,警报传感器的安放位置: + +```java +// enums/AlarmPoints.java +package enums; +public enum AlarmPoints { + STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3, + OFFICE4, BATHROOM, UTILITY, KITCHEN +} +``` + +然后,我们用 EnumSet 来跟踪报警器的状态: + +```java +// enums/EnumSets.java +// Operations on EnumSets +// {java enums.EnumSets} +package enums; +import java.util.*; +import static enums.AlarmPoints.*; +public class EnumSets { + public static void main(String[] args) { + EnumSet points = + EnumSet.noneOf(AlarmPoints.class); // Empty + points.add(BATHROOM); + System.out.println(points); + points.addAll( + EnumSet.of(STAIR1, STAIR2, KITCHEN)); + System.out.println(points); + points = EnumSet.allOf(AlarmPoints.class); + points.removeAll( + EnumSet.of(STAIR1, STAIR2, KITCHEN)); + System.out.println(points); + points.removeAll( + EnumSet.range(OFFICE1, OFFICE4)); + System.out.println(points); + points = EnumSet.complementOf(points); + System.out.println(points); + } +} +``` + +输出为: + +```java +[BATHROOM] +[STAIR1, STAIR2, BATHROOM, KITCHEN] +[LOBBY, OFFICE1, OFFICE2, OFFICE3, OFFICE4, BATHROOM, +UTILITY] +[LOBBY, BATHROOM, UTILITY] +[STAIR1, STAIR2, OFFICE1, OFFICE2, OFFICE3, OFFICE4, +KITCHEN] +``` + +使用 static import 可以简化 enum 常量的使用。EnumSet 的方法的名字都相当直观,你可以查阅 JDK 文档找到其完整详细的描述。如果仔细研究了 EnunSet 的文档,你还会发现 of() 方法被重载了很多次,不但为可变数量参数进行了重载,而且为接收 2 至 5 个显式的参数的情况都进行了重载。这也从侧面表现了 EnumSet 对性能的关注。因为,其实只使用单独的 of() 方法解决可变参数已经可以解决整个问题了,但是对比显式的参数,会有一点性能损失。采用现在这种设计,当你只使用 2 到 5 个参数调用 of() 方法时,你可以调用对应的重载过的方法(速度稍快一点),而当你使用一个参数或多过 5 个参数时,你调用的将是使用可变参数的 of() 方法。注意,如果你只使用一个参数,编译器并不会构造可变参数的数组,所以与调用只有一个参数的方法相比,也就不会有额外的性能损耗。 + +EnumSet 的基础是 long,一个 long 值有 64 位,而一个 enum 实例只需一位 bit 表示其是否存在。 +也就是说,在不超过一个 long 的表达能力的情况下,你的 EnumSet 可以应用于最多不超过 64 个元素的 enum。如果 enum 超过了 64 个元素会发生什么呢? + +```java +// enums/BigEnumSet.java +import java.util.*; +public class BigEnumSet { + enum Big { A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, + A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, + A20, A21, A22, A23, A24, A25, A26, A27, A28, A29, + A30, A31, A32, A33, A34, A35, A36, A37, A38, A39, + A40, A41, A42, A43, A44, A45, A46, A47, A48, A49, + A50, A51, A52, A53, A54, A55, A56, A57, A58, A59, + A60, A61, A62, A63, A64, A65, A66, A67, A68, A69, + A70, A71, A72, A73, A74, A75 } + public static void main(String[] args) { + EnumSet bigEnumSet = EnumSet.allOf(Big.class); + System.out.println(bigEnumSet); + } +} +``` + +输出为: + +```java +[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, +A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, +A24, A25, A26, A27, A28, A29, A30, A31, A32, A33, A34, +A35, A36, A37, A38, A39, A40, A41, A42, A43, A44, A45, +A46, A47, A48, A49, A50, A51, A52, A53, A54, A55, A56, +A57, A58, A59, A60, A61, A62, A63, A64, A65, A66, A67, +A68, A69, A70, A71, A72, A73, A74, A75] +``` + +显然,EnumSet 可以应用于多过 64 个元素的 enum,所以我猜测,Enum 会在必要的时候增加一个 long。 + ## 使用 EnumMap +EnumMap 是一种特殊的 Map,它要求其中的键(key)必须来自一个 enum,由于 enum 本身的限制,所以 EnumMap 在内部可由数组实现。因此 EnumMap 的速度很快,我们可以放心地使用 enum 实例在 EnumMap 中进行查找操作。不过,我们只能将 enum 的实例作为键来调用 put() 可方法,其他操作与使用一般的 Map 差不多。 + +下面的例子演示了*命令设计模式*的用法。一般来说,命令模式首先需要一个只有单一方法的接口,然后从该接口实现具有各自不同的行为的多个子类。接下来,程序员就可以构造命令对象,并在需要的时候使用它们了: + +```java +// enums/EnumMaps.java +// Basics of EnumMaps +// {java enums.EnumMaps} +package enums; +import java.util.*; +import static enums.AlarmPoints.*; +interface Command { void action(); } +public class EnumMaps { + public static void main(String[] args) { + EnumMap em = + new EnumMap<>(AlarmPoints.class); + em.put(KITCHEN, + () -> System.out.println("Kitchen fire!")); + em.put(BATHROOM, + () -> System.out.println("Bathroom alert!")); + for(Map.Entry e: + em.entrySet()) { + System.out.print(e.getKey() + ": "); + e.getValue().action(); + } + try { // If there's no value for a particular key: + em.get(UTILITY).action(); + } catch(Exception e) { + System.out.println("Expected: " + e); + } + } +} +``` + +输出为: + +``` +BATHROOM: Bathroom alert! +KITCHEN: Kitchen fire! +Expected: java.lang.NullPointerException +``` + +与 EnumSet 一样,enum 实例定义时的次序决定了其在 EnumMap 中的顺序。 + +main0 方法的最后部分说明,enum 的每个实例作为一个键,总是存在的。但是,如果你没有为这个键调用 put() 方法来存人相应的值的话,其对应的值就是 null。 + +与常量相关的方法(constant-specific methods 将在下一节中介绍)相比,EnumMap 有一个优点,那 EnumMap 允许程序员改变值对象,而常量相关的方法在编译期就被固定了。稍后你会看到,在你有多种类型的 enum,而且它们之间存在互操作的情况下,我们可以用 EnumMap 实现多路分发(multiple dispatching)。 + ## 常量特定方法 +Java 的 enum 有一个非常有趣的特性,即它允许程序员为 enum 实例编写方法,从而为每个 enum 实例赋予各自不同的行为。要实现常量相关的方法,你需要为 enum 定义一个或多个 abstract 方法,然后为每个 enum 实例实现该抽象方法。参考下面的例子: + +```java +// enums/ConstantSpecificMethod.java +import java.util.*; +import java.text.*; +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()); + } +} +``` + +输出为: + +```java +May 9, 2017 +C:\Users\Bruce\Documents\GitHub\on- +java\ExtractedExamples\\gradle\wrapper\gradle- +wrapper.jar +1.8.0_112 +``` + +通过相应的 enum 实例,我们可以调用其上的方法。这通常也称为表驱动的代码(table-driven code,请注意它与前面提到的命令模式的相似之处)。 + +在面向对象的程序设计中,不同的行为与不同的类关联。而通过常量相关的方法,每个 enum 实例可以具备自己独特的行为,这似乎说明每个 enum 实例就像一个独特的类。在上面的例子中,enum 实例似乎被当作其“超类”ConstantSpecificMethod 来使用,在调用 getInfo() 方法时,体现出多态的行为。 + +然而,enum 实例与类的相似之处也仅限于此了。我们并不能真的将 enum 实例作为一个类型来使用: + +```java +// enums/NotClasses.java +// {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(); +} +public class NotClasses { + // void f1(LikeClasses.WINKEN instance) {} // Nope +} +``` + +输出为(前 12 行): + +``` +Compiled from "NotClasses.java" +abstract class LikeClasses extends +java.lang.Enum { +public static final LikeClasses WINKEN; +public static final LikeClasses BLINKEN; +public static final LikeClasses NOD; +public static LikeClasses[] values(); +Code: +0: getstatic #2 // Field +$VALUES:[LLikeClasses; +3: invokevirtual #3 // Method +"[LLikeClasses;".clone:()Ljava/lang/Object; +... +``` + +在方法 f1() 中,编译器不允许我们将一个 enum 实例当作 class 类型。如果我们分析一下编译器生成的代码,就知道这种行为也是很正常的。因为每个 enum 元素都是一个 LikeClasses 类型的 static final 实例。 + +同时,由于它们是 static 实例,无法访问外部类的非 static 元素或方法,所以对于内部的 enum 的实例而言,其行为与一般的内部类并不相同。 + +再看一个更有趣的关于洗车的例子。每个顾客在洗车时,都有一个选择菜单,每个选择对应一个不同的动作。可以将一个常量相关的方法关联到一个选择上,再使用一个 EnumSet 来保存客户的选择: + +```java +// enums/CarWash.java +import java.util.*; +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 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(); + } +} +``` + +输出为: + +``` +[BASIC, RINSE] +The basic wash +Rinsing +[BASIC, HOTWAX, RINSE, BLOWDRY] +The basic wash +Applying hot wax +Rinsing +Blowing dry +``` + +与使用匿名内部类相比较,定义常量相关方法的语法更高效、简洁。 + +这个例子也展示了 EnumSet 了一些特性。因为它是一个集合,所以对于同一个元素而言,只能出现一次,因此对同一个参数重复地调用 add0 方法会被忽略掉(这是正确的行为,因为一个 bit 位开关只能“打开”一次),同样地,向 EnumSet 添加 enum 实例的顺序并不重要,因为其输出的次序决定于 enum 实例定义时的次序。 + +除了实现 abstract 方法以外,程序员是否可以覆盖常量相关的方法呢?答案是肯定的,参考下面的程序: + +```java +// enums/OverrideConstantSpecific.java +public enum OverrideConstantSpecific { + NUT, BOLT, + WASHER { + @Override + void f() { + System.out.println("Overridden method"); + } + }; + void f() { + System.out.println("default behavior"); + } + public static void main(String[] args) { + for(OverrideConstantSpecific ocs : values()) { + System.out.print(ocs + ": "); + ocs.f(); + } + } +} +``` + +输出为: + +``` +NUT: default behavior +BOLT: default behavior +WASHER: Overridden method +``` + +虽然 enum 有某些限制,但是一般而言,我们还是可以将其看作是类。 + +### 使用 enum 的职责链 + +在职责链(Chain of Responsibility)设计模式中,程序员以多种不同的方式来解决一个问题,然后将它们链接在一起。当一个请求到来时,它遍历这个链,直到链中的某个解决方案能够处理该请求。 + +通过常量相关的方法,我们可以很容易地实现一个简单的职责链。我们以一个邮局的模型为例。邮局需要以尽可能通用的方式来处理每一封邮件,并且要不断尝试处理邮件,直到该邮件最终被确定为一封死信。其中的每一次尝试可以看作为一个策略(也是一个设计模式),而完整的处理方式列表就是一个职责链。 + +我们先来描述一下邮件。邮件的每个关键特征都可以用 enum 来表示。程序将随机地生成 Mail 对象,如果要减小一封邮件的 GeneralDelivery 为 YES 的概率,那最简单的方法就是多创建几个不是 YES 的 enum 实例,所以 enum 的定义看起来有点古怪。 + +我们看到 Mail 中有一个 randomMail() 方法,它负责随机地创建用于测试的邮件。而 generator() 方法生成一个 Iterable 对象,该对象在你调用 next() 方法时,在其内部使用 randomMail() 来创建 Mail 对象。这样的结构使程序员可以通过调用 Mail.generator() 方法,很容易地构造出一个 foreach 循环: + +```java +// enums/PostOffice.java +// Modeling a post office +import java.util.*; +import onjava.*; +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 generator(final int count) { + return new Iterable() { + int n = count; + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return n-- > 0; + } + @Override + public Mail next() { + return randomMail(); + } + @Override + public void remove() { // Not implemented + throw new UnsupportedOperationException(); + } + }; + } + }; + } +} +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("*****"); + } + } +} +``` + +输出为: + +``` +Mail 0, General Delivery: NO2, Address Scanability: +UNSCANNABLE, Address Readability: YES3, Address +Address: OK1, Return address: OK1 +Delivering Mail 0 normally +***** +Mail 1, General Delivery: NO5, Address Scanability: +YES3, Address Readability: ILLEGIBLE, Address Address: +OK5, Return address: OK1 +Delivering Mail 1 automatically +***** +Mail 2, General Delivery: YES, Address Scanability: +YES3, Address Readability: YES1, Address Address: OK1, +Return address: OK5 +Using general delivery for Mail 2 +***** +Mail 3, General Delivery: NO4, Address Scanability: +YES3, Address Readability: YES1, Address Address: +INCORRECT, Return address: OK4 +Returning Mail 3 to sender +***** +Mail 4, General Delivery: NO4, Address Scanability: +UNSCANNABLE, Address Readability: YES1, Address +Address: INCORRECT, Return address: OK2 +Returning Mail 4 to sender +***** +Mail 5, General Delivery: NO3, Address Scanability: +YES1, Address Readability: ILLEGIBLE, Address Address: +OK4, Return address: OK2 +Delivering Mail 5 automatically +***** +Mail 6, General Delivery: YES, Address Scanability: +YES4, Address Readability: ILLEGIBLE, Address Address: +OK4, Return address: OK4 +Using general delivery for Mail 6 +***** +Mail 7, General Delivery: YES, Address Scanability: +YES3, Address Readability: YES4, Address Address: OK2, +Return address: MISSING +Using general delivery for Mail 7 +***** +Mail 8, General Delivery: NO3, Address Scanability: +YES1, Address Readability: YES3, Address Address: +INCORRECT, Return address: MISSING +Mail 8 is a dead letter +***** +Mail 9, General Delivery: NO1, Address Scanability: +UNSCANNABLE, Address Readability: YES2, Address +Address: OK1, Return address: OK4 +Delivering Mail 9 normally +***** +``` + +职责链由 enum MailHandler 实现,而 enum 定义的次序决定了各个解决策略在应用时的次序。对每一封邮件,都要按此顺序尝试每个解决策略,直到其中一个能够成功地处理该邮件,如果所有的策略都失败了,那么该邮件将被判定为一封死信。 + +### 使用 enum 的状态机 + +枚举类型非常适合用来创建状态机。一个状态机可以具有有限个特定的状态,它通常根据输入,从一个状态转移到下一个状态,不过也可能存在瞬时状态(transient states),而一旦任务执行结束,状态机就会立刻离开瞬时状态。 + +每个状态都具有某些可接受的输入,不同的输入会使状态机从当前状态转移到不同的新状态。由于 enum 对其实例有严格限制,非常适合用来表现不同的状态和输入。一般而言,每个状态都具有一些相关的输出。 + +自动售贷机是一个很好的状态机的例子。首先,我们用一个 enum 定义各种输入: + +```java +// enums/Input.java +import java.util.*; +public enum Input { + 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() {} + int amount() { return value; }; // In cents + static Random rand = new Random(47); + public static Input randomSelection() { + // Don't include STOP: + return values()[rand.nextInt(values().length - 1)]; + } +} +``` + +注意,除了两个特殊的 Input 实例之外,其他的 Input 都有相应的价格,因此在接口中定义了 amount(方法。然而,对那两个特殊 Input 实例而言,调用 amount(方法并不合适,所以如果程序员调用它们的 amount)方法就会有异常抛出(在接口内定义了一个方法,然后在你调用该方法的某个实现时就会抛出异常),这似乎有点奇怪,但由于 enum 的限制,我们不得不采用这种方式。 + +VendingMachine 对输入的第一个反应是将其归类为 Category enum 中的某一个 enum 实例,这可以通过 switch 实现。下面的例子演示了 enum 是如何使代码变得更加清晰且易于管理的: + +```java +// enums/VendingMachine.java +// {java VendingMachine VendingMachineInput.txt} +import java.util.*; +import java.io.IOException; +import java.util.function.*; +import java.nio.file.*; +import java.util.stream.*; +enum Category { + 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 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); + } +} + +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 gen) { + while(state != State.TERMINAL) { + state.next(gen.get()); + while(state.isTransient) + state.next(); + state.output(); + } + } + public static void main(String[] args) { + Supplier gen = new RandomInputSupplier(); + if(args.length == 1) + gen = new FileInputSupplier(args[0]); + run(gen); + } +} + +// For a basic sanity check: +class RandomInputSupplier implements Supplier { + @Override + public Input get() { + return Input.randomSelection(); + } +} + +// Create Inputs from a file of ';'-separated strings: +class FileInputSupplier implements Supplier { + private Iterator 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()); + } +} +``` + +输出为: + +``` +25 +50 +75 +here is your CHIPS +0 +100 +200 +here is your TOOTHPASTE +0 +25 +35 +Your change: 35 +0 +25 +35 +Insufficient money for SODA +35 +60 +70 +75 +Insufficient money for SODA +75 +Your change: 75 +0 +Halted +``` + +由于用 switch 语句从 enum 实例中进行选择是最常见的一种方式(请注意,为了使 enum 在 switch 语句中的使用变得简单,我们是需要付出其他代价的),所以,我们经常遇到这样的问题:将多个 enum 进行分类时,“我们希望在什么 enum 中使用 switch 语句?”我们通过 VendingMachine 的例子来研究一下这个问题。对于每一个 State,我们都需要在输入动作的基本分类中进行查找:用户塞入钞票,选择了某个货物,操作被取消,以及机器停止。然而,在这些基本分类之下,我们又可以塞人不同类型的钞票,可以选择不同的货物。Category enum 将不同类型的 Input 进行分组,因而,可以使用 categorize0 方法为 switch 语句生成恰当的 Cateroy 实例。并且,该方法使用的 EnumMap 确保了在其中进行查询时的效率与安全。 + +如果读者仔细研究 VendingMachine 类,就会发现每种状态的不同之处,以及对于输入的不同响应,其中还有两个瞬时状态。在 run() 方法中,状态机等待着下一个 Input,并一直在各个状态中移动,直到它不再处于瞬时状态。 + +通过两种不同的 Generator 对象,我们可以使用不同的 Supplier 对象来测试 VendingMachine,首先是 RandomInputSupplier,它会不停地生成除了 SHUT-DOWN 之外的各种输入。通过长时间地运行 RandomInputSupplier,可以起到健全测试(sanity test)的作用,能够确保该状态机不会进入一个错误状态。另一个是 FileInputSupplier,使用文件以文本的方式来描述输入,然后将它们转换成 enum 实例,并创建对应的 Input 对象。上面的程序使用的正是如下的文本文件: + +``` +// enums/VendingMachineInput.txt +QUARTER; QUARTER; QUARTER; CHIPS; +DOLLAR; DOLLAR; TOOTHPASTE; +QUARTER; DIME; ABORT_TRANSACTION; +QUARTER; DIME; SODA; +QUARTER; DIME; NICKEL; SODA; +ABORT_TRANSACTION; +STOP; +``` + +FileInputSupplier 构造函数将此文件转换为流,并跳过注释行。然后它使用 String.split() 以分号进行分割。这会生成一个 String 数组,并可以通过将其转换为 Stream,然后应用 flatMap() 来将其输入到流中。其输出结果将去除所有空格空格,并转换为 List\,且从中获取 Iterator\。 + +这种设计有一个缺陷,它要求 enum State 实例访问的 VendingMachine 属性必须声明为 static,这意味着,你只能有一个 VendingMachine 实例。不过如果我们思考一下实际的(嵌入式 Java)应用,这也许并不是一个大问题,因为在一台机器上,我们可能只有一个应用程序。 -## 多次调度 +## 多路分发 + +当你要处理多种交互类型时,程序可能会变得相当杂乱。举例来说,如果一个系统要分析和执行数学表达式。我们可能会声明 Number.plus(Number),Number.multiple(Number) 等等,其中 Number 是各种数字对象的超类。然而,当你声明 a.plus(b) 时,你并不知道 a 或 b 的确切类型,那你如何能让它们正确地交互呢? + +你可能从未思考过这个问题的答案.Java 只支持单路分发。也就是说,如果要执行的操作包含了不止一个类型未知的对象时,那么 Java 的动态绑定机制只能处理其中一个的类型。这就无法解决我们上面提到的问题。所以,你必须自己来判定其他的类型,从而实现自己的动态线定行为。 + +解决上面问题的办法就是多路分发(在那个例子中,只有两个分发,一般称之为两路分发).多态只能发生在方法调用时,所以,如果你想使用两路分发,那么就必须有两个方法调用:第一个方法调用决定第一个未知类型,第二个方法调用决定第二个未知的类型。要利用多路分发,程序员必须为每一个类型提供一个实际的方法调用,如果你要处理两个不同的类型体系,就需要为每个类型体系执行一个方法调用。一般而言,程序员需要有设定好的某种配置,以便一个方法调用能够引出更多的方法调用,从而能够在这个过程中处理多种类型。为了达到这种效果,我们需要与多个方法一同工作:因为每个分发都需要一个方法调用。在下面的例子中(实现了 “石头、剪刀、布”游戏,也称为 RoShamBo)对应的方法是 compete() 和 eval(),二者都是同一个类型的成员,它们可以产生三种 Outcome 实例中的一个作为结果: + +```java +// enums/Outcome.java +package enums; +public enum Outcome { WIN, LOSE, DRAW } +// enums/RoShamBo1.java +// Demonstration of multiple dispatching +// {java enums.RoShamBo1} +package enums; + import java.util.*; + import static enums.Outcome.*; +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 DRAW; } + @Override + public Outcome eval(Scissors s) { return WIN; } + @Override + public Outcome eval(Rock r) { return 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 LOSE; } + @Override + public Outcome eval(Scissors s) { return DRAW; } + @Override + public Outcome eval(Rock r) { return 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 WIN; } + @Override + public Outcome eval(Scissors s) { return LOSE; } + @Override + public Outcome eval(Rock r) { return DRAW; } + @Override + public String toString() { return "Rock"; } +} +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(); + } + } + 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()); + } +} +``` + +输出为: + +``` +Rock vs. Rock: DRAW +Paper vs. Rock: WIN +Paper vs. Rock: WIN +Paper vs. Rock: WIN +Scissors vs. Paper: WIN +Scissors vs. Scissors: DRAW +Scissors vs. Paper: WIN +Rock vs. Paper: LOSE +Paper vs. Paper: DRAW +Rock vs. Paper: LOSE +Paper vs. Scissors: LOSE +Paper vs. Scissors: LOSE +Rock vs. Scissors: WIN +Rock vs. Paper: LOSE +Paper vs. Rock: WIN +Scissors vs. Paper: WIN +Paper vs. Scissors: LOSE +Paper vs. Scissors: LOSE +Paper vs. Scissors: LOSE +Paper vs. Scissors: LOSE +``` + +Item 是这几种类型的接口,将会被用作多路分发。RoShamBo1.match() 有两个 Item 参数,通过调用 Item.compete90) 方法开始两路分发。要判定 a 的类型,分发机制会在 a 的实际类型的 compete(内部起到分发的作用。compete() 方法通过调用 eval() 来为另一个类型实现第二次分法。 + +将自身(this)作为参数调用 evalo,能够调用重载过的 eval() 方法,这能够保留第一次分发的类型信息。当第二次分发完成时,你就能够知道两个 Item 对象的具体类型了。 + +要配置好多路分发需要很多的工序,不过要记住,它的好处在于方法调用时的优雅的话法,这避免了在一个方法中判定多个对象的类型的丑陋代码,你只需说,“嘿,你们两个,我不在乎你们是什么类型,请你们自己交流!”不过,在使用多路分发前,请先明确,这种优雅的代码对你确实有重要的意义。 + +### 使用 enum 分发 + +直接将 RoShamBol.java 翻译为基于 enum 的版本是有问题的,因为 enum 实例不是类型,不能将 enum 实例作为参数的类型,所以无法重载 eval() 方法。不过,还有很多方式可以实现多路分发,并从 enum 中获益。 + +一种方式是使用构造器来初始化每个 enum 实例,并以“一组”结果作为参数。这二者放在一块,形成了类似查询表的结构: + +```java +// enums/RoShamBo2.java +// Switching one enum on another +// {java enums.RoShamBo2} +package enums; +import static enums.Outcome.*; +public enum RoShamBo2 implements Competitor { + 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; + } + @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); + } +} +``` + +输出为: + +``` +ROCK vs. ROCK: DRAW +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +PAPER vs. PAPER: DRAW +PAPER vs. SCISSORS: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. SCISSORS: DRAW +ROCK vs. SCISSORS: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. PAPER: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +``` + +在 compete() 方法中,一旦两种类型都被确定了,那么唯一的操作就是返回结果 Outcome 然而,你可能还需要调用其他的方法,(例如)甚至是调用在构造器中指定的某个命令对象上的方法。 + +RoShamBo2.javal 之前的例子短小得多,而且更直接,更易于理解。注意,我们仍然是使用两路分发来判定两个对象的类型。在 RoShamBol.java 中,两次分发都是通过实际的方法调用实现,而在这个例子中,只有第一次分发是实际的方法调用。第二个分发使用的是 switch,不过这样做是安全的,因为 enum 限制了 switch 语句的选择分支。 + +在代码中,enum 被单独抽取出来,因此它可以应用在其他例子中。首先,Competitor 接口定义了一种类型,该类型的对象可以与另一个 Competitor 相竞争: + +```java +// enums/Competitor.java +// Switching one enum on another +package enums; +public interface Competitor> { + Outcome compete(T competitor); +} +``` + +然后,我们定义两个 static 方法(static 可以避免显式地指明参数类型),第一个是 match() 方法,它会为一个 Competitor 对象调用 compete() 方法,并与另一个 Competitor 对象作比较。在这个例子中,我们看到,match())方法的参数需要是 Competitor\ 类型。但是在 play() 方法中,类型参数必须同时是 Enum\ 类型(因为它将在 Enums.random() 中使用)和 Competitor\ 类型因为它将被传递给 match() 方法): + +```java +// enums/RoShamBo.java +// Common tools for RoShamBo examples +package enums; +import onjava.*; +public class RoShamBo { + public static > + void match(T a, T b) { + System.out.println( + a + " vs. " + b + ": " + a.compete(b)); + } + public static & Competitor> + void play(Class rsbClass, int size) { + for(int i = 0; i < size; i++) + match(Enums.random(rsbClass),Enums.random(rsbClass)); + } +} +``` + +play() 方法没有将类型参数 T 作为返回值类型,因此,似乎我们应该在 Class\ 中使用通配符来代替上面的参数声明。然而,通配符不能扩展多个基类,所以我们必须采用以上的表达式。 + +### 使用常量相关的方法 + +常量相关的方法允许我们为每个 enum 实例提供方法的不同实现,这使得常量相关的方法似乎是实现多路分发的完美解决方案。不过,通过这种方式,enum 实例虽然可以具有不同的行为,但它们仍然不是类型,不能将其作为方法签名中的参数类型来使用。最好的办法是将 enum 用在 switch 语句中,见下例: + +```java +// enums/RoShamBo3.java +// Using constant-specific methods +// {java enums.RoShamBo3} +package enums; +import static enums.Outcome.*; +public enum RoShamBo3 implements Competitor { + PAPER { + @Override + public Outcome compete(RoShamBo3 it) { + switch(it) { + default: // To placate the compiler + case PAPER: return DRAW; + case SCISSORS: return LOSE; + case ROCK: return WIN; + } + } + }, + SCISSORS { + @Override + public Outcome compete(RoShamBo3 it) { + switch(it) { + default: + case PAPER: return WIN; + case SCISSORS: return DRAW; + case ROCK: return LOSE; + } + } + }, + ROCK { + @Override + public Outcome compete(RoShamBo3 it) { + switch(it) { + default: + case PAPER: return LOSE; + case SCISSORS: return WIN; + case ROCK: return DRAW; + } + } + }; + @Override + public abstract Outcome compete(RoShamBo3 it); + public static void main(String[] args) { + RoShamBo.play(RoShamBo3.class, 20); + } +} +``` + +输出为: + +``` +ROCK vs. ROCK: DRAW +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +PAPER vs. PAPER: DRAW +PAPER vs. SCISSORS: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. SCISSORS: DRAW +ROCK vs. SCISSORS: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. PAPER: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +``` + +虽然这种方式可以工作,但是却不甚合理,如果采用 RoShamB02.java 的解决方案,那么在添加一个新的类型时,只需更少的代码,而且也更直接。 + +:然而,RoShamBo3.java 还可以压缩简化一下: + +```java +// enums/RoShamBo4.java +// {java enums.RoShamBo4} +package enums; +public enum RoShamBo4 implements Competitor { + 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); + } + }; + 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); + } +} +``` + +输出为: + +``` +PAPER vs. PAPER: DRAW +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. SCISSORS: WIN +ROCK vs. ROCK: DRAW +ROCK vs. SCISSORS: WIN +PAPER vs. SCISSORS: LOSE +SCISSORS vs. SCISSORS: DRAW +PAPER vs. SCISSORS: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. ROCK: WIN +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +``` + +其中,具有两个参数的 compete() 方法执行第二个分发,该方法执行一系列的比较,其行为类似 switch 语句。这个版本的程序更简短,不过却比较难理解,对于一个大型系统而言,难以理解的代码将导致整个系统不够健壮。 + +### 使用 EnumMap 进行分发 + +使用 EnumMap 能够实现“真正的”两路分发。EnumMap 是为 enum 专门设计的一种性能非常好的特殊 Map。由于我们的目的是摸索出两种未知的类型,所以可以用一个 EnumMap 的 EnumMap 来实现两路分发: + +```java +// enums/RoShamBo5.java +// Multiple dispatching using an EnumMap of EnumMaps +// {java enums.RoShamBo5} +package enums; +import java.util.*; +import static enums.Outcome.*; +enum RoShamBo5 implements Competitor { + PAPER, SCISSORS, ROCK; + static EnumMap> + table = new EnumMap<>(RoShamBo5.class); + static { + for(RoShamBo5 it : RoShamBo5.values()) + table.put(it, new EnumMap<>(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 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); + } +} +``` + +输出为: + +``` +ROCK vs. ROCK: DRAW +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +PAPER vs. PAPER: DRAW +PAPER vs. SCISSORS: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. SCISSORS: DRAW +ROCK vs. SCISSORS: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. PAPER: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +``` + +该程序在一个 static 子句中初始化 EnumMap 对象,具体见表格似的 initRow() 方法调用。请注意 compete() 方法,您可以看到,在一行语句中发生了两次分发。 + +### 使用二维数组 + +我们还可以进一步简化实现两路分发的解决方案。我们注意到,每个 enum 实例都有一个固定的值(基于其声明的次序),并且可以通过 ordinal() 方法取得该值。因此我们可以使用二维数组,将竞争者映射到竞争结果。采用这种方式能够获得最简洁、最直接的解决方案(很可能也是最快速的,虽然我们知道 EnumMap 内部其实也是使用数组实现的)。 + +```java +We can simplify the solution even more by noting that each enum instance has a fixed + value (based on its declaration order) and that ordinal() produces this value. A two- + dimensional array mapping the competitors onto the outcomes produces the smallest + and most straightforward solution (and possibly the fastest, although remember that + EnumMap uses an internal array): +// enums/RoShamBo6.java +// Enums using "tables" instead of multiple dispatch +// {java enums.RoShamBo6} + package enums; + import static enums.Outcome.*; +enum RoShamBo6 implements Competitor { + PAPER, SCISSORS, ROCK; + private static Outcome[][] table = { + { DRAW, LOSE, WIN }, // PAPER + { WIN, DRAW, LOSE }, // SCISSORS + { LOSE, WIN, 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); + } +} +``` + +输出为: + +``` +ROCK vs. ROCK: DRAW +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +PAPER vs. PAPER: DRAW +PAPER vs. SCISSORS: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. SCISSORS: DRAW +ROCK vs. SCISSORS: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. PAPER: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +``` + +table 与前一个例子中 initRow() 方法的调用次序完全相同。 + +与前面一个例子相比,这个程序代码虽然简短,但表达能力却更强,部分原因是其代码更易于理解与修改,而且也更直接。不过,由于它使用的是数组,所以这种方式不太“安全”。如果使用一个大型数组,可能会不小心使用了错误的尺寸,而且,如果你的测试不能覆盖所有的可能性,有些错误可能会从你眼前溜过。 + +事实上,以上所有的解决方案只是各种不同类型的表罢了。不过,分析各种表的表现形式,找出最适合的那一种,还是很有价值的。注意,虽然上例是最简洁的一种解决方案,但它也是相当僵硬的方案,因为它只能针对给定的常量输入产生常量输出。然而,也没有什么特别的理由阻止你用 table 来生成功能对象。对于某类问题而言,“表驱动式编码”的概念具有非常强大的功能。 + ## 本章小结 +虽然枚举类型本身并不是特别复杂,但我还是将本章安排在全书比较靠后的位置,这是因为,程序员可以将 enum 与 Java 语言的其他功能结合使用,例如多态、泛型和反射。 + +虽然 Java 中的枚举比 C 或 C++中的 enum 更成熟,但它仍然是一个“小”功能,Java 没有它也已经(虽然有点笨拙)存在很多年了。而本章正好说明了一个“小”功能所能带来的价值。有时恰恰因为它,你才能够优雅而干净地解决问题。正如我在本书中一再强调的那样,优雅与清晰很重要,正是它们区别了成功的解决方案与失败的解决方案。而失败的解决方案就是因为其他人无法理解它。 + +关于清晰的话题,Java 1.0 对术语 enumeration 的选择正是一个不幸的反例。对于一个专门用于从序列中选择每一个元素的对象而言,Java 竟然没有使用更通用、更普遍接受的术语 ierator 来表示它(参见[集合 ]() 章节),有些语言甚至将枚举的数据类型称为 “enumerators”!Java 修正了这个错误,但是 Enumeration 接口已经无法轻易地抹去了,因此它将一直存在于旧的(甚至有些新的)代码、类库以及文档中。 +