关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用。这是一种非常有用的功能。
使用enum可以做很多有趣的事情,同时,我们也会深入其他的Java特性,例如泛型和反射。在这个过程中,我们还将学习一些设计模式。
1.基本enum特性
调用enum的values()方法,可以遍历enum实例。values()方法返回enum实例的数组,而且该数组中的元素严格保持其在enum中声明时的顺序,因此你可以在循环中使用values()返回的数组。
创建enum时,编译器会为你生成一个相关的类,这个类继承自java.lang.Enum。下面的例子演示了Enum提供的一些功能:
package com.zjwave.thinkinjava.enumerated;
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实例,如果不存在给定名字的实例,将会抛出异常。
1.1 将静态导入用于enum
先看以下示例:
package com.zjwave.thinkinjava.enumerated;
public enum Spiciness {
NOT, MULD, MEDIUM, HOT, FLAMING
}
package com.zjwave.thinkinjava.enumerated;
import static com.zjwave.thinkinjava.enumerated.Spiciness.*;
public class Burrito {
Spiciness degree;
public Burrito(Spiciness degree) {
this.degree = degree;
}
@Override
public String toString() {
return "Burrito is " + degree;
}
public static void main(String[] args) {
System.out.println(new Burrito(NOT));
System.out.println(new Burrito(MEDIUM));
System.out.println(new Burrito(HOT));
}
}
使用static import能够将enum实例的标识符带入当前的命名空间,所以无需再用enum类型来修饰enum实例。这是一个好的想法吗?或者还是显示地修饰enum示例更好?这要看代码的复杂程度了。编译器可以确保你使用的是正确的类型,所以唯一需要担心的是,使用静态导入会不会导致你的代码令人难以理解。多数情况下,使用static import还是有好处的,不过,程序员还是应该对具体情况进行具体分析。
注意,在定义enum的同一个文件中,这种技巧无法使用,如果是在默认包中定义enum,这种技巧也无法使用。
2.向enum中添加新方法
除了不能继承自一个enum之外,我们基本上可以将enum看做一个常规的类。也就是说,我们可以向enum中添加方法。enum甚至可以有main()方法。
一般来说,我们希望每个枚举实例能够返回对自身的描述,而不仅仅只是默认的toString()实现,这只能返回枚举实例的名字。为此,你可以提供一个构造器,专门负责处理这个额外的信息,然后添加一个方法,返回这个描述信息。看一看下面的示例:
package com.zjwave.thinkinjava.enumerated;
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 Ryby " +
"Slippers, crushed by Dorothy's House"),
SOUTH("Good by inference, by missing");
private String description;
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实例序列的最后添加一个分号。同时,Java要求你必须先定义enum实例。如果在定义enum实例之前定义了任何方法或属性,那么在编译时就会得到错误信息。
enum中的构造器与方法和普通的类没有区别,因为除了有少许限制之外,enum就是一个普通的类。所以,我们可以使用enum做许多事情(虽然,我们一般只使用普通的枚举类型)。
在这个例子中,虽然外面有意识地将enum的构造器声明为private,但对于它的可访问性而言,其实并没有什么变化,因为(即使不声明为private)我们只能在enum定义的内部使用其构造器创建enum实例。一旦enum的定义结束,编译器就不允许我们再使用其构造器来创建任何实例了。
2.1 覆盖enum的方法
覆盖toString()方法,给我们提供了另一种方式来为枚举实例是生成不同的字符串描述信息。在下面的实例中,我们使用的就是实例的名字,不过我们希望改变其格式。覆盖enum的toString()方法与覆盖一般类的方法没有区别:
package com.zjwave.thinkinjava.enumerated;
public enum SpaceShip {
SCOUT, CARGO, TRANSPORT, CRUISER, BATTLESHIP, MOTHERSHIP;
@Override
public String toString() {
String id = name();
String lower = id.substring(1).toLowerCase();
return id.charAt(0) + lower;
}
public static void main(String[] args) {
for (SpaceShip s : values()) {
System.out.println(s);
}
}
}
toString()方法通过调用name()方法取得SpaceShip的名字,然后将其修改为只有首字母大写的格式。
3.switch语句中的enum
在switch中使用enum,是enum提供的一项非常便利的功能。一般来说,在switch中只能使用整数值,而枚举实例天生就具备整数值的次序,并且可以通过ordinal()方法取得其次序(显然编译器帮我们做了类似的工作),因此我们可以在switch语句中使用enum。
虽然一般情况下我们必须使用enum类型来修饰一个enum实例,但是在case语句中却不必如此。下面的例子使用enum构造了一个小型状态机:
package com.zjwave.thinkinjava.enumerated;
public class TrafficLight {
Signal color = Signal.RED;
public void change(){
switch (color){
// Note that 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
}
编译器并没有抱怨switch中没有default语句,但这并不是因为每一个Signal都有对应的case语句。如果你注释掉其中的某个case语句,编译器同样不会抱怨什么。这意味着,你必须确保自己覆盖了所有的分支。但是,如果在case语句中调用return,那么编译器就会抱怨缺少default语句了。这与是否覆盖了enum的所有实例无关。
4.values()的神秘之处
前面已经提到,编译器为你创建的enum类都继承自Enum类。然而,如果你研究一下Enum类就会发现,它并没有values()方法。可我们明明已经用过该方法了,难道存在某种“隐藏的”方法吗?我们可以利用反射机制编写一个简单的程序,来查看其中的究竟:
package com.zjwave.thinkinjava.enumerated;
import com.zjwave.thinkinjava.io.Directory;
import com.zjwave.thinkinjava.io.OSExecute;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.TreeSet;
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: ");
TreeSet<String> methods = new TreeSet<>();
for (Method method : enumCLass.getMethods()) {
methods.add(method.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:
String path = Directory.walk(".","Explore.class").files.get(0).getAbsolutePath();
String command = "javap " + path;
OSExecute.command(command);
}
}
enum Explore {
HERE, THERE
}
答案是,values()是由编译器添加的static方法。可以看出,在创建Explore的过程中,编译器还为其添加了valueOf()方法。这可能有点令人迷惑,Enum类不是已经有valueOf()方法了吗。不过Enum中的valueOf()方法需要两个参数,而这个新增的方法只需一个参数。由于这里使用的Set只存储方法的名字,而不考虑方法的签名,所以在调用Explore.removeAll(Enum)之后,就只剩下[values]了。
从最后的输出中可以看到,编译器将Explore标记为final类,所以无法继承自enum。其中还有一个static的初始化子句,稍后我们将学习如何重定义该句。
由于values()方法是由编译器插入到enum定义中的static方法,所以,如果你将enum实例向上转型为Enum,那么values()方法就不可访问了。不过,在Class中有一个getEnumConstants()方法,所以即便Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有enum实例:
package com.zjwave.thinkinjava.enumerated;
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上的方法,所以你甚至可以对不是枚举的类调用此方法:
package com.zjwave.thinkinjava.enumerated;
public class NonEnum {
public static void main(String[] args) {
Class<Integer> intClass = Integer.class;
try {
for (Integer en : intClass.getEnumConstants()) {
System.out.println(en);
}
}catch (Exception e){
System.out.println(e);
}
}
}
只不过,此时该方法返回null,所以当你试图使用其返回的结果时会发生异常。
5.实现,而非继承
我们已经知道,所有的enum都继承自java.lang.Enum类。由于Java不支持多重继承,所以你的enum不能再继承其他类:
enum NotPossible extends Pet{...// Won't work
然而,在我们创建一个新的enum时,可以同时实现一个或多个接口:
package com.zjwave.thinkinjava.enumerated.cartoons;
import com.zjwave.thinkinjava.generics.Generator;
import java.util.Random;
public class EnumImplementation {
public static <T> void printNext(Generator<T> rg){
System.out.print(rg.next() + ", ");
}
public static void main(String[] args) {
// Choose any instance:
CartoonCharacter cc = CartoonCharacter.BOB;
for (int i = 0; i < 10; i++) {
printNext(cc);
}
}
}
enum CartoonCharacter implements Generator<CartoonCharacter> {
SLAPPY, SPANKY, PUNCHY, SILLY, BOUNCY, NUTTY, BOB;
private Random rand = new Random(47);
@Override
public CartoonCharacter next() {
return values()[rand.nextInt(values().length)];
}
}
这个结果有点奇怪,不过你必须要有一个enum实例才能调用其上的方法。现在,在任何接受Generator参数的方法中,例如printNext(),都可以使用CartoonCharacter。
6.随机选取
就像你在CartoonCharacter.next()中看到的那样,本文中很多示例都需要从enum实例中进行随机选择。我们可以利用泛型,从而使得这个工作更一般化。
package com.zjwave.thinkinjava.enumerated;
import java.util.Random;
import java.util.regex.Pattern;
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)];
}
}
古怪的语法<T extends Enum<T>>表示T是一个enum实例。而将Class<T>作为参数的话,我们就可以利用Class对象得到enum实例的数组了。重载后的random()方法只需使用T[]作为参数,因为它并不会调用Enum上的任何操作,它只需从数组中随机选择一个元素即可。这样,最终返回类型正式enum的类型。
下面是random()方法的一个简单示例:
package com.zjwave.thinkinjava.enumerated;
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
}
虽然Enums只是一个相当短小的类,但是在本文中你会发现,它能消除很多重复的代码。重复总会制造麻烦,因此消除重复总是有益处的。
7.使用接口组织枚举
无法从enum继承子类有时很令人沮丧。这种需求有时源自我们希望扩展原enum中的元素,有时是因为我们希望使用子类将一个enum中的元素进行分组。
在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组,可以达到将枚举元素分类组织的目的。举例来说,假设你想用enum来表示不同类别的食物,同时还希望每个enum元素仍然保持Food类型。那可以这样实现:
package com.zjwave.thinkinjava.enumerated;
public interface Food {
enum Appetizer implements Food {
SALAD, SOUP, SPRING_ROLLS
}
enum MainCourse implements Food {
LASAGNE, BURRITO, PAD_THAI,
LENTILS, HUMMOUS, VINDALOO
}
enum Dessert implements Food {
TIRAMISU, GELATO, BLACK_FOREST_CAKE,
FRUIT, CREME_CARAMEL
}
enum Coffee implements Food {
BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
LATTE, CAPPUCCINO, TEA, HERB_TEA
}
}
对于enum而言,实现接口是使其子类话的唯一办法,所以嵌入在Food中的每个enum都实现了Food接口。现在,在下面的程序中,我们可以说“所有东西都是某种类型的Food”:
package com.zjwave.thinkinjava.enumerated;
import static com.zjwave.thinkinjava.enumerated.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类:
package com.zjwave.thinkinjava.enumerated;
public enum Course {
APPETIZER(Food.Appetizer.class),
MAINCOURSE(Food.MainCourse.class),
DESSERT(Food.Dessert.class),
COFFEE(Food.Coffee.class);
private Food[] values;
Course(Class<? extends Food> kind) {
values = kind.getEnumConstants();
}
public Food randomSelection(){
return Enums.random(values);
}
}
在上面的程序中,每一个Course的实例都将其对应的Class对象作为构造器的参数。通过getEnumConstants()方法,可以从该Class对象中取得某个Food子类的所有enum实例。这些实例在randomSelection()中被用到。因此,通过从每一个Course实例中随机地选择一个Food,我们便能够生成一份菜单:
package com.zjwave.thinkinjava.enumerated;
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("---");
}
}
}
在这个例子中,我们通过遍历每一个Course实例来获得“枚举的枚举”的值。稍后,在VendingMachine.java中,我们会看到另一种组织枚举实例的方式,但其也有一些其他的限制。
此外,还有一种更简洁的管理枚举的办法,就是将一个enum嵌套在另一个enum内。就像这样:
package com.zjwave.thinkinjava.enumerated;
enum SecurityCategory {
STOCK(Security.Stock.class),
BOUND(Security.Bond.class)
;
Security[] values;
SecurityCategory(Class<? extends Security> kind) {
this.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());
}
}
}
Security接口的作用是将其所包含的enum组合成一个公共类型,这一点是有必要的。然后,SecurityCategory才能将Security中的enum作为其构造器的参数使用,以起到组织的效果。
如果我们将这种方式应用于Food的例子,结果应该这样;
package com.zjwave.thinkinjava.enumerated;
public enum Meal2 {
APPETIZER(Food.Appetizer.class),
MAINCOURSE(Food.MainCourse.class),
DESSERT(Food.Dessert.class),
COFFEE(Food.Coffee.class)
;
private Food[] values;
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("---");
}
}
}
其实,这仅仅是重新组织了一下代码,不过多数情况下,这种方式使你的代码具有更清晰的结构。
8.使用EnumSet标志
Set是一种集合,只能向其中添加不重复的对象。当然,enum也要求其成员都是唯一的,所以enum看起来也具有集合的行为。不过,由于不能从enum中删除或添加元素,所以它只能算是不太有用的集合。Java SE5引入EnumSet,是为了通过enum创建一种替代品,以代替传统的基于int的“位标志”。这种标志可以用来表示某种“开/关”信息,不过,使用这种标志,我们最终操作的只是一些bit,而不是这些bit想要表达的概念,因此,很容易写出令人难以理解的代码。
EnumSet的设计充分考虑到了速度因素,因为它必须与非常高效的bit标志相竞争(其操作与HashSet相比,非常地快)。就其内部而言,它(可能)就是将一个long值作为比特向量,所以EnumSet非常快速高效。使用EnumSet的有点是,它在说明一个二进制位是否存在时,具有更好的表达能力,并且无需担心性能。
EnumSet中的元素必须来自一个enum。下面的enum表示在一座大楼中,警报传感器的安放位置:
package com.zjwave.thinkinjava.enumerated;
public enum AlarmPoints {
STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3, OFFICE4,
BATHROOM, UTILITY, KITCHEN
}
然后,我们用EnumSet来跟中报警器的状态:
package com.zjwave.thinkinjava.enumerated;
import java.util.EnumSet;
import static com.zjwave.thinkinjava.enumerated.AlarmPoints.*;
public class EnumSets {
public static void main(String[] args) {
EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class);//Empty set
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);
}
}
使用static import可以简化enum常量的使用。EnumSet的方法的名字都相当直观,你可以查阅JDK文档找到其完整详细的描述。如果仔细研究了EnumSet的文档,你还会发现一个有趣的地方:of()方法被重载了很多次,不但为可变数量参数进行了重载,而且为接收2到5个显示的参数的情况都进行了重载。这也从侧面表现了EnumSet对性能的关注。因为,其实只使用可变参数已经可以解决整个问题了,但是对比显式的参数,会有一点性能损失。采用现在这种设计,当你只使用2到5个参数调用of()方法时,你可以调用对应的重载过的方法(速度稍快一点),而当你使用一个参数或多过5个参数时,你调用的将是使用可变参数的of()方法。注意,如果你只使用一个参数,编译器并不会构造可变参数的数组,所以与调用只有一个参数的方法相比,也就不会有额外的性能消耗。
EnumSet的基础是long,一个long值有64位,而一个enum实例只需一位bit表示其是否存在。也就是说,在不超过一个long的表达能力的情况下,你的EnumSet可以应用于最多不超过64个元素的enum。如果enum超过了64个元素会发生什么呢?
package com.zjwave.thinkinjava.enumerated;
import java.util.EnumSet;
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<Big> bigEnumSet = EnumSet.allOf(Big.class);
System.out.println(bigEnumSet);
}
}
显然,EnumSet可以应用于多过64个元素的enum,所以,我猜测在大于64个元素的enum使用EnumSet时,会使用另一种实现,浏览EnumSet源码你会发现,确实如此。
/**
* Creates an enum set containing all of the elements in the specified
* element type.
*
* @param <E> The class of the elements in the set
* @param elementType the class object of the element type for this enum
* set
* @return An enum set containing all the elements in the specified type.
* @throws NullPointerException if <tt>elementType</tt> is null
*/
public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {
EnumSet<E> result = noneOf(elementType);
result.addAll();
return result;
}
/**
* Creates an empty enum set with the specified element type.
*
* @param <E> The class of the elements in the set
* @param elementType the class object of the element type for this enum
* set
* @return An empty enum set of the specified type.
* @throws NullPointerException if <tt>elementType</tt> is null
*/
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
9.使用EnumMap
EnumMap是一种特殊的Map,它要求其中的键(key)必须来自一个enum。由于enum本身的限制,所以EnumMap在内部可由数组实现。因此EnumMap的速度很快,我们可以放心地使用enum实例在EnumMap中进行查找操作。不过,我们只能将enum的实例作为键来调用put()方法,其他操作与使用一般的Map差不多。
下面的例子演示了命令设计模式的用法。一般来说,命令模式首先需要一个只有单一方法的接口,然后从该接口实现具有各自不同的行为的多个子类。接下来,程序员就可以构造命令对象,并在需要的时候使用它们了:
package com.zjwave.thinkinjava.enumerated;
import java.util.EnumMap;
import java.util.Map;
import static com.zjwave.thinkinjava.enumerated.AlarmPoints.*;
public class EnumMaps {
public static void main(String[] args) {
EnumMap<AlarmPoints,Command> em = new EnumMap<>(AlarmPoints.class);
em.put(KITCHEN, new Command() {
@Override
public void action() {
System.out.println("Kitchen fire!");
}
});
em.put(BATHROOM, new Command() {
@Override
public void action() {
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(UTILITY).action();
}catch (Exception e){
System.out.println(e);
}
}
}
interface Command{
void action();
}
与EnumSet一样,enum实例定义时的次序决定了其在EnumMap中的顺序。
main()方法的最后部分说明,enum的每个实例作为一个键,总是存在的。但是,如果你没有为这个键调用put()方法来存入相应的值的话,其对应的值就是null。
与常量相关的方法相比,EnumMap有一个优点,就是EnumMap允许程序员改变值对象,而常量相关的方法在编译期就被固定了。
稍后你会看到,在你有多种类型的enum,而且它们之间存在互操作的情况下,我们可以用EnumMap实现多路分发(multiple dispatching)。
10.常量相关的方法
Java的enum有一个非常有趣的特性,即它允许程序员为enum实例编写方法,从而为每个enum实例赋予各自不同的行为。要实现常量相关的方法,你需要为enum定义一个或多个abstract方法,然后为每个enum实例实现该抽象方法。参考下面的例子:
package com.zjwave.thinkinjava.enumerated;
import java.text.DateFormat;
import java.util.Date;
public enum ConstantSpecificMethod {
DATE_TIME{
@Override
String getInfo() {
return DateFormat.getDateInstance().format(new Date());
}
},
CLASSPATH{
@Override
String getInfo() {
return System.getenv("JAVA_HOME");
}
},
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实例,我们可以调用其上的相关方法。这通常也成为表驱动的代码(tabledriven code,请注意它与前面提到的命令模式的相似之处)。
在面向对象的程序设计中,不同的行为与不同的类关联。而通过常量相关的方法,每个enum实例可以具备自己独特的行为,这似乎说明每个enum实例就像一个独特的类。在上面的例子中,enum实例似乎被当做其“超类”ConstantSpecificMethod来使用,在调用getInfo()方法时,体现出多态的行为。
然而,enum实例与类的相似之处也仅限于此了。我们并不能真的将enum实例作为一个类型来使用:
package com.zjwave.thinkinjava.enumerated;
public class NotClasses {
//void f1(LikeClasses.WINKEN instance){};//Nope
}
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();
}
以下是LikeClasses的反编译信息:
Compiled from "NotClasses.java"
abstract class com.zjwave.thinkinjava.enumerated.LikeClasses extends java.lang.Enum<com.zjwave.thinkinjava.enumerated.LikeClasses> {
public static final com.zjwave.thinkinjava.enumerated.LikeClasses WINKEN;
public static final com.zjwave.thinkinjava.enumerated.LikeClasses BLINKEN;
public static final com.zjwave.thinkinjava.enumerated.LikeClasses NOD;
public static com.zjwave.thinkinjava.enumerated.LikeClasses[] values();
public static com.zjwave.thinkinjava.enumerated.LikeClasses valueOf(java.lang.String);
abstract void behavior();
com.zjwave.thinkinjava.enumerated.LikeClasses(java.lang.String, int, com.zjwave.thinkinj
ava.enumerated.LikeClasses$1);
static {};
}
在方法f1()中,编译器不允许我们将一个enum实例当做class类型。如果我们分析一下编译器生成的代码,就知道这种行为也是很正常的。因为每个enum元素都是一个LikeClasses类型的static final实例。
同时,由于它们是static实例,无法访问外部类的非static元素或方法,所以对于内部的enum的实例而言,其行为与一般的内部类并不相同。
再看一个关于洗车的例子。每个顾客在洗车时,都有一个选择菜单,每个选择对应一个不同的动作。可以将一个常量相关的方法关联到一个选择上,再使用一个EnumSet来保存客户的选择:
package com.zjwave.thinkinjava.enumerated;
import java.util.EnumSet;
public class CarWash {
public enum Cycle{
UNDERBODY{
@Override
void action() {
System.out.println("Spraying the underbody");
}
},
WHEELWHASH{
@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();
}
}
与使用匿名内部类相比较,定义常量相关方法的语法更高效、简洁。
这个例子也展示了EnumSet的一些特性。因为它是一个Set,所以对于同一个元素而言,只能出现一次,因此对同一个参数重复地调用add()方法会被忽略掉(这是正确的行为,因为一个bit位开关只能“打开”一次)。同样地,向EnumSet添加enum实例的顺序并不重要,因为其输出的次序决定于enum实例定义时的次序。
除了实现abstract方法以外,程序员是否可以覆盖常量相关的方法呢?答案是肯定的,参考下面的程序:
package com.zjwave.thinkinjava.enumerated;
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();
}
}
}
虽然enum有某些限制,但是一般而言,我们还是可以将其看作是类。
10.1 使用enum的职责链
在职责链(Chain of Responsibility)设计模式中,程序员以多种不同的方式来解决一个问题,然后将它们链接在一起。当一个请求到来时,它遍历这个链,知道链中的某个解决方案能够处理该请求。
通过常量相关的方法,我们可以很容易地实现一个简单的职责链。我们以一个邮局的模型为例。邮局需要以尽可能通用的方式来处理每一封邮件,并且要不断尝试处理邮件,直到该邮件最终被确定为一封死信。其中的每一次尝试可以看作为一个策略(也是一个设计模式),而完整的处理方式列表就是一个职责链。
我们先来描述一下邮件。邮件的每个关键特征都可以用enum来表示。程序将随机地生成Mail对象,如果要减小一封邮件的GeneralDelivery为YES的概率,那最简单的方法就是多创建几个不是YES的实例,所以enum的定义看起来有点古怪。
我们看到Mail中有一个randomMail()方法,它负责随机地创建用于创建测试的邮件。而generator()方法生成一个Iterable对象,该对象在你调用next()方法时,在其内部使用randomMail()来创建Mail对象。这样的结构使程序员可以通过调用Mail.generator()方法,很容易地构造出一个foreach循环:
package com.zjwave.thinkinjava.enumerated;
import java.util.Iterator;
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 + " aotomatically");
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("Return " + 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 lower the 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 Scannablity: " + 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>() {
@Override
public Iterator<Mail> iterator() {
return new Iterator<Mail>() {
int n = count;
@Override
public boolean hasNext() {
return n-- > 0;
}
@Override
public Mail next() {
return randomMail();
}
};
}
};
}
}
职责链是由enum MailHandler实现,而enum定义的次序决定了各个解决策略在应用时的次序。对每一封邮件,都要按此顺序尝试每个解决策略,知道其中一个能够成功地处理该邮件,如果所有的策略都失败了,那么该邮件将被判定为一封死信。
10.3 使用enum的状态机
枚举类型非常适合用来创建状态机。一个状态机可以具有有限个特定的状态,它通常根据输入,从一个状态转移到下一个状态,不过也可能存在瞬时状态(transient states),而一旦任务执行结束,状态机就会立刻离开瞬时状态。
每个状态都具有某些可接受的输入,不同的输入会使状态机从当前状态转移到不同的新状态。由于enum对其实例有严格限时,非常适合用来表现不同的状态和输入。一般而言,每个状态都具有一些相关的输出。
自动售货机是一个很好的状态机的例子。首先,我们用一个enum定义各种输入:
package com.zjwave.thinkinjava.enumerated;
import java.util.Random;
public enum Input {
NICKEL(5),DIME(10),QUARTER(25),
DOLLAR(100),TOOTHPASTE(200),
CHIPS(75),SODA(100),SOAP(50),
ABORT_TRANSACTION {
@Override
int amount() {//Disallow
throw new RuntimeException("ABORT.amount()");
}
},
STOP{// This must be the last instance.
@Override
int amount() {
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是如何使代码变得更加清晰且易于管理的:
package com.zjwave.thinkinjava.enumerated;
import com.zjwave.thinkinjava.generics.Generator;
import com.zjwave.thinkinjava.io.TextFile;
import java.util.EnumMap;
import java.util.Iterator;
import static com.zjwave.thinkinjava.enumerated.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(Generator<Input> gen){
while (state != State.TERMINAL){
state.next(gen.next());
while (state.isTransient){
state.next();
}
state.output();
}
}
/**
* Args:VendingMachineInput.txt
* @param args
*/
public static void main(String[] args) {
Generator<Input> gen = new RandomInputGenerator();
if(args.length == 1){
gen = new FileInputGenerator(args[0]);
}
run(gen);
}
// For a basic sanity check:
static class RandomInputGenerator implements Generator<Input>{
@Override
public Input next() {
return Input.randomSelection();
}
}
// Create Inputs form a file of ';'-separated strings:
static class FileInputGenerator implements Generator<Input>{
private Iterator<String> input;
FileInputGenerator(String fileName){
input = new TextFile(fileName,";").iterator();
}
@Override
public Input next() {
if(!input.hasNext()){
return null;
}
return Enum.valueOf(Input.class,input.next().trim());
}
}
}
enum Category {
MONEY(NICKEL, DIME, QUARTER, DOLLAR),
ITEM_SELECTION(TOOTHPASTE, CHIPS, SODA, SOAP),
QUIT_TRANSACTION(ABORT_TRANSACTION),
SHUT_DOWN(STOP);
private Input[] values;
Category(Input... values) {
this.values = values;
}
private static EnumMap<Input, Category> categorys = new EnumMap<>(Input.class);
static {
for (Category c : Category.class.getEnumConstants()) {
for (Input type : c.values) {
categorys.put(type, c);
}
}
}
public static Category categorize(Input input) {
return categorys.get(input);
}
}
由于switch语句从enum实例中进行选择是最常见的一种方式(请注意,为了使enum在switch语句中的使用变得简单,我们是需要付出其他代价的),所以,我们经常遇到这样的问题:将多个enum进行分类时,“我们希望在什么enum中使用switch语句?”我们通过VendingMachine的例子来研究一下这个问题。对于每一个State,我们都需要在输入动作的基本分类中进行查找:用户塞入钞票,选择了某个货物,操作被取消,以及机器停止。然而,在这些基本分类之下,我们又可以塞入不同类型的钞票,可以选择不同的货物。Category enum将不同类型的Input进行分组,因而,可以使用categorize()方法为switch语句生成恰当的Category实例。并且,该方法使用的EnumMap确保了在其中进行查询时的效率与安全。
如果读者仔细研究VendingMachine类,就会发现每种状态的不同之处,以及对于输入的不同响应,其中还有两个瞬时状态。在run()方法中,状态机等待着下一个Input,并一直在各个状态中移动,知道它不再处于瞬时状态。
通过两种不同的Generator对象,我们可以用两种方式来测试VendingMachine。首先是RandomInputGenerator,它会不停地生成各种输入,当然,除了SHUT_DOWN之外。通过长时间地运行RandomInputGenerator,可以起到健全测试(sanity test)的作用,能够确保该状态机不会进入一个错误状态。另一个是FileInputGenerator,使用文件以文本的方式来描述输入,然后将它们转换成enum实例,并创建对应的Input对象。上面的程序使用的正是名为VendingMachineInput.txt内容如下的文本文件:
QUARTER; QUARTER; QUARTER; CHIPS;
DOLLAR; DOLLAR; TOOTHPASTE;
QUARTER; DIME; ABORT_TRANSACTION;
QUARTER; DIME; SODA;
QUARTER; DIME; NICKEL; SODA;
ABORT_TRANSACTION;
STOP;
这种设计有一个缺陷,它要求enum State实例访问的VendingMachine属性必须声明为static,这意味着,你只能有一个VendingMachine实例。不过如果我们思考一下实际的(嵌入式Java)应用,这也许并不是一个大问题,因为在一台机器上,我们可能只有一个应用程序。
11.多路分发
当你要处理多种交互类型时,程序可能会变得相当杂乱。举例来说,如果一个系统要分析和执行数学表达式。我们可能会声明Number.plus(Number)、Number.multiple(Number)等等,其中Number是各种数字对象的超类。然而,当你声明a.plus(b)时,你并不知道a或b的确切类型,那你如何能让它们正确地交互呢?
你可能从未思考过这个问题的答案。Java只支持单路分发。也就是说,如果要执行的操作包含了不止一个类型未知的对象时,那么Java的动态绑定机制只能处理其中一个的类型。这就无法解决我们上面提到的问题。所以,你必须自己来判定其他的类型,从而实现自己的动态绑定行为。
解决上面问题的办法就是多路分发(在上面那个例子中,只有两个分发,一般称之为两路分发)。多态只能发生在方法调用时,所以,如果你想使用两路分发,那么就必须有两个方法调用:第一个方法调用决定第一个未知类型,第二个方法调用决定第二个未知的类型。要利用多路分发,程序员必须为每一个类型提供一个实际的方法调用,如果你要处理两个不同的类型体系,就需要为每个类型体系执行一个方法调用。一般而言,程序员需要有设定好的某种配置,以便一个方法调用能够引出更多的方法调用,从而能够在这个过程中处理多种类型。为了达到这种效果,我们需要与多个方法一同工作:因为每个分发都需要一个方法调用。在下面的例子中(实现了“石头、剪刀、布”游戏,也成为RoShamBo)对应的方法是compete()和eval(),而这都是同一个类型的成员,它们可以产生三种Outcom实例中的一个作为结果:
package com.zjwave.thinkinjava.enumerated;
import java.util.Random;
import static com.zjwave.thinkinjava.enumerated.Outcome.*;
public class RoShamBo1 {
static final int SIZE = 20;
private static Random rand = new Random(47);
public static Item newItem() {
switch (rand.nextInt(3)) {
case 0:
return new Scissors();
case 1:
return new Paper();
case 2:
return new Rock();
default:
return null;
}
}
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());
}
}
}
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";
}
}
Item是这几种类型的接口,将会被用作多路分发。RoShamBo1.match()有两个Item参数,通过调用Item.compete()方法开始两路分发。要判定a的类型,分发机制会在a的实际类型的compete()内部起到分发的作用。compete()方法通过调用eval()来为另一个类型实现第二次分发。将自身(this)作为参数调用eval(),能够调用重载过的eval()方法,这能够保留第一次分发的类型信息。当第二次分发完成时,你就能够知道两个Item对象的具体类型了。
要配置好多路分发需要很多的工序,不过要记住,它的好处在于方法调用时的优雅的语法,这避免了在一个方法中判定多个对象的类型的丑陋代码,你只需说,“嘿,你们两个,我不在乎你们是什么类型,请你们自己交流!”不过,在使用多路分发前,请先明确,这种优雅的代码对你确实有重要的意义。
11.1 使用enum分发
直接将RoShambo1.java翻译为基于enum的版本是有问题的,因为enum实例不是类型,不能将enum实例作为参数的类型,所以无法重载eval()方法。不过,还有很多方式可以实现多路分发,并从enum中获益。
一种方式是使用构造器来初始化每个enum实例,并以“一组”结果作为参数。这二者放在一块,形成了类似查询表的结构:
package com.zjwave.thinkinjava.enumerated;
import static com.zjwave.thinkinjava.enumerated.Outcome.*;
public enum RoShamBo2 implements Competitor<RoShamBo2> {
PAPER(DRAW, LOSE, WIN),
SCISSORS(WIN, DRAW, LOSE),
ROCK(LOSE, WIN, DRAW);
private Outcome vPAPER, vSCISSORS, vROCK;
RoShamBo2(Outcome vPAPER, Outcome vSCISSORS, Outcome vROCK) {
this.vPAPER = vPAPER;
this.vSCISSORS = vSCISSORS;
this.vROCK = vROCK;
}
@Override
public Outcome compete(RoShamBo2 it) {
switch (it) {
case PAPER:
return vPAPER;
case SCISSORS:
return vSCISSORS;
case ROCK:
return vROCK;
default:
return null;
}
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo2.class,20);
}
}
在compete()方法中,一旦两种类型都被确定了,那么唯一的操作就是返回结果Outcome。然而,你可能还需要调用其他的方法,(例如)甚至是调用在构造器中指定的某个命令对象上的方法。
RoShamBo2.java比之前的例子短小得多,而且更直接,更易于理解。注意,我们仍然是使用两路分发来判定两个对象的类型。在RoShamBo1.java中,两次分发都是通过实际的方法调用实现,而在这个例子中,只有第一次分发是实际的方法调用。第二个分发使用的是switch,不过这样做是安全的,因为enum限制了switch语句的选择分支。
在代码中,enum被单独抽取出来,因此它可以应用在其他例子中。首先Competitor接口定义了一种类型,该类型的对象可以与另一个Competitor相竞争;
package com.zjwave.thinkinjava.enumerated;
public interface Competitor<T extends Competitor<T>> {
Outcome compete(T competitor);
}
然后,我们定义两个static方法(static可以避免显式地指明参数类型)。第一个是match()方法,它会为一个Competitor对象调用compete()方法,并与另一个Competitor对象作比较。在这个例子中,我们看到,match()方法的参数需要是Competitor<T>类型。但是在play()方法中,类型参数必须同时是Enum<T>类型(因为它将在Enums.random()中使用)和Competitor<T>类型(因为它将被传递给match()方法):
package com.zjwave.thinkinjava.enumerated;
public class RoShamBo {
public static <T extends Competitor<T>> void match(T a,T b){
System.out.println(a + " vs. " + b + ": " + a.compete(b));
}
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));
}
}
}
play()方法没有将类型的参数T作为返回值类型,因此,似乎我们应该在Class<T>中使用通配符来代替上面的参数声明。然而,通配符不能扩展多个基类,所以我们必须采用以上的表达式。
11.2 使用常量相关的方法
常量相关的方法允许我们为每个enum实例提供方法的不同实现,这使得常量相关的方法似乎是实现多路分发的完美解决方案。不过,通过这种方式,enum实例虽然可以具有不同的行为,但它们仍然不是类型,不能将其作为方法签名中的参数类型来使用。最好的办法是将enum用在switch语句中,见下例:
package com.zjwave.thinkinjava.enumerated;
import static com.zjwave.thinkinjava.enumerated.Outcome.*;
public enum RoShamBo3 implements Competitor<RoShamBo3> {
PAPER {
@Override
public Outcome compete(RoShamBo3 it) {
switch (it) {
case PAPER:
return DRAW;
case SCISSORS:
return LOSE;
case ROCK:
return WIN;
}
return null;
}
},
SCISSORS {
@Override
public Outcome compete(RoShamBo3 it) {
switch (it) {
case PAPER:
return WIN;
case SCISSORS:
return DRAW;
case ROCK:
return LOSE;
}
return null;
}
},
ROCK {
@Override
public Outcome compete(RoShamBo3 it) {
switch (it) {
case PAPER:
return LOSE;
case SCISSORS:
return WIN;
case ROCK:
return DRAW;
}
return null;
}
};
public abstract Outcome compete(RoShamBo3 it);
public static void main(String[] args) {
RoShamBo.play(RoShamBo3.class,20);
}
}
虽然这种方式可以工作,但是却不甚合理,如果采用RoShamBo2.java的解决方案,那么在添加一个新的类型时,只需更少的代码,而且也更直接。
然而,RoShamBo3.java还可以压缩简化一下:
package com.zjwave.thinkinjava.enumerated;
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);
}
}
;
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);
}
}
其中,具有两个参数的compete()方法执行第二个分发,该方法执行一系列的比较,其行为类似switch语句。这个版本的程序更简短,不过却比较难理解。对于一个大型系统而言,难以理解的代码将导致整个系统不够简装。
11.3 使用EnumMap分发
使用EnumMap能够实现“真正的”两路分发。EnumMap是为enum专门设计的一种性能非常好的特殊Map。由于我们的蒂姆的摸索出两种未知的类型,所以可以用一个EnumMap的EnumMap来实现两路分发:
package com.zjwave.thinkinjava.enumerated;
import java.util.EnumMap;
import static com.zjwave.thinkinjava.enumerated.Outcome.*;
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,DRAW,LOSE,WIN);
initRow(SCISSORS,WIN,DRAW,LOSE);
initRow(ROCK,LOSE,WIN,DRAW);
}
static void initRow(RoSHamBo5 it,Outcome vPAPER,Outcome vSCISSORS,Outcome vROCK){
EnumMap<RoSHamBo5, Outcome> row = RoSHamBo5.table.get(it);
row.put(RoSHamBo5.PAPER,vPAPER);
row.put(RoSHamBo5.SCISSORS,vSCISSORS);
row.put(RoSHamBo5.ROCK,vROCK);
}
@Override
public Outcome compete(RoSHamBo5 it) {
return table.get(this).get(it);
}
public static void main(String[] args) {
RoShamBo.play(RoSHamBo5.class,20);
}
}
该程序在一个static子句中初始化EnumMap对象,具体见表格似的initRow()方法调用。请注意compete()方法,你可以看到,在一行语句中发生了两次分发。
11.4 使用二维数组
我们还可以进一步简化实现两路分发的解决方案。我们注意到,每个enum实例都有一个固定的值(基于其声明的次序),并且可以通过ordinal()方法取得该值。因此我们可以使用二维数组,将竞争者映射到竞争结果。采用这种方式能够获得最简洁、最直接的解决方案(很可能也是最快速的,虽然外面知道EnumMap内部其实也是使用数组实现的)。
package com.zjwave.thinkinjava.enumerated;
import static com.zjwave.thinkinjava.enumerated.Outcome.*;
public enum RoShamBo6 implements Competitor<RoShamBo6> {
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);
}
}
table与前一个例子中initRow()方法的调用次序完全相同。
与前面一个例子相比,这个程序代码虽然简短,但表达能力却更强,部分原因是其代码更易于理解与修改,而且也更直接。不过,由于它使用的是数组,所以这种方式不太“安全”。如果使用一个大型数组,可能会不小心使用了错误的尺寸,而且,如果你的测试不能覆盖所有的可能性,有些错误可能会从你眼前溜走。
事实上,以上所有的解决方案只是各种不同类型的表罢了。不过,分析各种表的表现形式,找出最适合的那一种,还是很有价值的。注意,虽然上例是最简洁的一种解决方案,但它也是相当僵硬的方案,因为它只能针对给定的常量输入产生常量输出。然而,也没有什么特别的理由阻止你用table来生成功能对象。对于某类问题而言,“表驱动式编码”的概念具有非常强大的功能。
12.总结
虽然枚举类型本身并不是特别复杂,但是程序员可以将enum与Java语言的其他功能结合使用,例如多态、泛型和反射。
虽然Java中的枚举比C或C++中的enum更成熟,但它仍然是一个“小”功能,Java没有它也已经(虽然有点笨拙)存在很多年了。而本文正好说明了一个“小”功能所能带来的价值。有时恰恰因为它,你才能够优雅而干净地解决问题。优雅与清晰确实很重要,正式它们区别了成功的解决方案与失败的解决方案。而失败的解决方案就是因为其他人无法理解它。
关于清晰的话题,Java 1.0对属于enumeration的选择正是一个不幸的反例。对于一个专门用于从序列中选择每一个元素的对象而言,Java竟然没有使用更通用、更普遍接受的术语iterator来表示它(参见集合)。有些语言甚至将枚举的数据类型称为enumerators。java修正了这个错误,但是Enumeration接口已经无法轻易地抹去了,因此它将一直存在于旧的(甚至有些新的)代码、类库以及文档中。
所有源码均可在https://gitee.com/zjwave/thinkinjava中下载
关联文章:
Thinking in Java——Java异常体系(通过异常处理错误)
Thinking in Java——String及相关类库的使用
Thinking in Java——运行时类型信息(RTTI)以及反射
转载请注明原文链接:ZJ-Wave
共有 0 条评论