红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Java编程笔记
  4. 正文

Java编程笔记19:枚举

2022年4月13日 258点热度 0人点赞 0条评论

5c9c3b3b392ac581.jpg

图源:PHP中文网

之前在Java编程笔记2:初始化和清理 - 魔芋红茶's blog (icexmoon.cn)中简单介绍过枚举,在对反射和泛型等内容学习后,我们可以更深入地学习和理解枚举。

基本操作

下面这个例子说明了一些枚举的基本操作:

package ch19.basic;
​
import util.Fmt;
​
enum Color {
    RED, BLUE, GREEN, YELLO, BLACK
}
​
public class Main {
    public static void main(String[] args) {
        Color comparedColor = Color.GREEN;
        for (Color color : Color.values()) {
            System.out.print(color + "\t");
            System.out.print("#" + color.ordinal() + "\t");
            String flag = "";
            int compare = color.compareTo(comparedColor);
            if (compare < 0) {
                flag = "<";
            } else if (compare == 0) {
                flag = "=";
            } else {
                flag = ">";
            }
            Fmt.printf("%s %s %s\t", color, flag, comparedColor);
            if (color == comparedColor) {
                flag = "=";
            } else {
                flag = "!=";
            }
            Fmt.printf("%s %s %s\t", color, flag, comparedColor);
            if (color.equals(comparedColor)) {
                flag = "=";
            } else {
                flag = "!=";
            }
            Fmt.printf("%s %s %s\t", color, flag, comparedColor);
            System.out.print(color.name() + "\t");
            System.out.print(color.getDeclaringClass());
            System.out.println();
        }
        Color c = Enum.valueOf(Color.class, "RED");
        System.out.println(c);
    }
}
// RED     #0      RED < GREEN     RED != GREEN    RED != GREEN    RED     class ch19.basic.Color
// BLUE    #1      BLUE < GREEN    BLUE != GREEN   BLUE != GREEN   BLUE    class ch19.basic.Color
// GREEN   #2      GREEN = GREEN   GREEN = GREEN   GREEN = GREEN   GREEN   class ch19.basic.Color
// YELLO   #3      YELLO > GREEN   YELLO != GREEN  YELLO != GREEN  YELLO   class ch19.basic.Color
// BLACK   #4      BLACK > GREEN   BLACK != GREEN  BLACK != GREEN  BLACK   class ch19.basic.Color
// RED

枚举类型有一个values方法,会返回一个所有枚举值组成的数组。可以利用它遍历所有的枚举值。

枚举值会按照定义的循序分配一个整型值,可以用ordinal方法获取。

枚举类型实现了Comparable接口,所以可以使用compareTo方法进行比较,比较结果与枚举对值的整型值的比较结果一致。

此外,可以通过compareTo比较两个枚举值是否相等,或者用==比较也是同样的效果。

枚举值的name方法会返回枚举值的字面量对应的字符串,getDeclaringClass方法可以获取枚举类型。

最后,可以用Enum.valueOf方法获取枚举值,其第一个参数是枚举类型的Class对象,第二个参数是枚举值字面量对应的字符串。

静态导入

通常在使用枚举值的时候必须带上枚举类型,比如Color.RED这样。实际上枚举值是枚举类型的静态属性,所以可以利用静态导入让使用枚举值的方式更简单:

package ch19.static1;

public enum Color {
    RED, GREEN, BLUE
}
package ch19.static1;

import static ch19.static1.Color.*;

public class Main {
    public static void main(String[] args) {
        System.out.println(RED);
        System.out.println(GREEN);
        System.out.println(BLUE);
        System.out.println(RED.getDeclaringClass());
    }
}
// RED
// GREEN
// BLUE
// class ch19.static1.Color

这个示例中枚举类型和Main是分别定义在一个包中的两个java文件中的,但其实在同一个文件中也可以静态导入枚举值,但似乎没有太大必要。

Enum

如果你使用javap查看枚举类型的字节码,就会看到类似下面这样的内容:

❯ javap -cp D:\workspace\java\java-notebook\xyz\icexmoon\java_notes ch19.methods2.Color
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Compiled from "Color.java"
public final class ch19.methods2.Color extends java.lang.Enum<ch19.methods2.Color> {
  public static final ch19.methods2.Color RED;
  public static final ch19.methods2.Color GREEN;
  public static final ch19.methods2.Color BLUE;
  public static ch19.methods2.Color[] values();
  public static ch19.methods2.Color valueOf(java.lang.String);
  static {};
}

这说明Java中的枚举类型实际上是一个继承自java.lang.Enum类的子类,并且被声明为final的,因此枚举类型不能被继承。此外,Enum还是一个泛型类,所以你可以看到,示例中出现Color extends Enum<Color>这样的内容,类似的写法在泛型部分我们解释过。

枚举值被以静态属性的方式定义,就像示例中的RED、GREEN那样。

最后,相比基类Enum,枚举类型还多出两个静态方法values和valueOf。

当然,这些实现细节对开发者是不可见的,都是由Java编译器实现的,我们只需要编写enum{...}这样的简单的枚举类型定义,编译器会自动帮我们将其转换为字节码中的这种定义。

了解上边这些内容后,我们可以通过反射的方式进一步进行验证:

package ch19.methods2;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import util.Fmt;

public class Main {
    public static void main(String[] args) {
        Set<String> subMethods = analysis(Color.class);
        Set<String> superMethods = analysis(Enum.class);
        subMethods.removeAll(superMethods);
        System.out.println(subMethods);
    }

    private static <T extends Enum> Set<String> analysis(Class<T> cls) {
        Fmt.printf("========analysis %s=========\n", cls.getName());
        System.out.println("super class:" + cls.getSuperclass().getName());
        System.out.println("interfaces:" + Arrays.toString(cls.getInterfaces()));
        Set<String> methods = new HashSet<>();
        for (Method method : cls.getMethods()) {
            methods.add(method.getName());
        }
        System.out.println("methods:" + methods);
        return methods;
    }
}
// ========analysis ch19.methods2.Color=========
// super class:java.lang.Enum
// interfaces:[]
// methods:[getClass, wait, valueOf, values, notifyAll, compareTo, describeConstable, notify, getDeclaringClass, hashCode, equals, name, toString, ordinal]
// ========analysis java.lang.Enum=========
// super class:java.lang.Object
// interfaces:[interface java.lang.constant.Constable, interface java.lang.Comparable, interface java.io.Serializable]
// methods:[getClass, wait, valueOf, notifyAll, compareTo, describeConstable, notify, getDeclaringClass, hashCode, equals, name, toString, ordinal]
// [values]

这个示例说明了上边的论点,之所以两个方法名称集合求差集后只有values没有valueOf,是因为实际上Enum也有一个静态方法valueOf,这点之前已经展示过了,不过它需要两个参数,而枚举类型只需要一个。

添加方法

就像上边说的,enum类型可以看作是一种特殊的类,除了存在一些限制外,大多数情况下可以当作普通的类来使用。这里边也包含添加普通方法和类方法,甚至包括构造函数:

package ch19.methods;

import java.util.Random;

enum Color {
    RED("this is a dack color."), BLUE, YELLOW("this is a light color."), BLACK, WHITE;

    private String des;
    private static Random rand = new Random();

    private Color(String des) {
        this.des = des;
    }

    private Color() {
        this("default description.");
    }

    public String getDes() {
        return this.des;
    }

    public static Color getRandomColor() {
        Color[] colors = values();
        return colors[rand.nextInt(colors.length)];
    }
}

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 6; i++) {
            Color c = Color.getRandomColor();
            System.out.print(c + " ");
            System.out.println(c.getDes());
        }
    }
}
// BLACK default description.
// BLUE default description.
// WHITE default description.
// BLUE default description.
// WHITE default description.
// YELLOW this is a light color.

要注意的是,为枚举类添加的构造函数必须是private的,否则无法通过编译。这样规定是合理的,因为实际上除了在枚举类型定义中添加枚举值,你是没法在其它地方添加枚举值的,自然其构造函数也就没有必要是private以外的访问权限。

还有要注意的是,在为枚举类型添加了其它属性和方法后,就需要在枚举值后添加一个;作为结束。

因为枚举值可以看作是一种“常量”,所以枚举类型的普通方法也被称作”特定常量方法“(constant-specific methods)。

覆盖Enum的方法

我们已经知道,枚举类型继承自Enum,因此,可以覆盖Enum的方法:

package ch19.methods3;
...
enum Color {
	...
    @Override
    public String toString() {
        return Fmt.sprintf("%s(%s)", name(), des);
    }
}

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 6; i++) {
            Color c = Color.getRandomColor();
            System.out.println(c);
        }
    }
}
// YELLOW(this is a light color.)
// RED(this is a dack color.)
// RED(this is a dack color.)
// YELLOW(this is a light color.)
// BLUE(default description.)
// BLACK(default description.)

switch

在Java编程笔记1:基础 - 魔芋红茶's blog (icexmoon.cn)中我们讨论了Java中switch语句的局限性,并且在Java编程笔记2:初始化和清理 - 魔芋红茶's blog (icexmoon.cn)中提到可以在switch语句中使用枚举类型。

下面是一个使用switch和枚举编写的红绿灯模拟示例:

package ch19.switch1;

enum TrafficLight {
    RED, YELLOW, GREEN
}

public class Main {
    public static void main(String[] args) {
        TrafficLight tl = TrafficLight.RED;
        StringBuilder sb= new StringBuilder();
        sb.append(tl);
        for (int i = 0; i < 10; i++) {
            tl = turn(tl);
            sb.append("=>");
            sb.append(tl);
        }
        System.out.println(sb.toString());
    }

    private static TrafficLight turn(TrafficLight tl) {
        switch (tl) {
            case RED:
                return TrafficLight.GREEN;
            case GREEN:
                return TrafficLight.YELLOW;
            case YELLOW:
                return TrafficLight.RED;
            default:
                return TrafficLight.RED;
        }
    }
}
// RED=>GREEN=>YELLOW=>RED=>GREEN=>YELLOW=>RED=>GREEN=>YELLOW=>RED=>GREEN

这里的红绿灯转换可以看作是一个简单的状态机,有意思的是,将枚举值用于switch块中的case关键字后时,可以省略掉枚举类型,这可能是因为switch条件中已经包含了一个枚举实例,编译器可以根据该实例确定其所属的枚举类型,所以就没必要在case后明确所属的枚举类型了。但是这也仅仅限于case条件语句,case之后的执行语句块中依然需要写明枚举类型。

values()

在前边已经介绍了编译器是如何实现枚举类型,以及如何给枚举类型添加了一个values()静态方法。这里再介绍一些相关示例。

因为values()方法是编译器添加的,而Enum实际上是没有该方法的,所以如果将一个枚举值向上转型为Enum类型,就无法调用这个方法。

package ch19.values;

enum Color {
    RED, YELLOW, GREEN, BLUE
}

public class Main {
    public static void main(String[] args) {
        Color[] colors = Color.RED.values();
        System.out.println(colors);
        Enum e = Color.RED;
        // colors = e.values();
    }
}

注释部分代码无法通过编译。

实际上这里不应该使用Color.RED.values这样的代码,实际上这是用对象调用静态方法,是不被推荐的方式。但在这个例子中,e.values()实际上也是试图在用一个Enum实例调用静态方法,所以是有意为之。

虽然在这个例子中,句柄已经从Color变成了Enum,产生了”细节丢失“,也因此无法调用values获取全部枚举值,但实际上e的真实类型依然是Color,并不会因为句柄的改变而改变(这也正是多态的精髓所在)。所以我们可以通过反射来还原真实类型,并获取相应的枚举值:

package ch19.values2;

import java.util.Arrays;

enum Color {
    RED, YELLOW, GREEN, BLUE
}

public class Main {
    public static void main(String[] args) {
        Color[] colors = Color.RED.values();
        System.out.println(Arrays.toString(colors));
        Enum e = Color.RED;
        // colors = e.values();
        Enum<Color>[] colors2 = e.getClass().getEnumConstants();
        System.out.println(Arrays.toString(colors2));
    }
}
// [RED, YELLOW, GREEN, BLUE]
// [RED, YELLOW, GREEN, BLUE]

Class.getEnumConstants方法可以获取枚举类型的全部枚举值,类似于values。但是前提必须是Class对象是一个枚举类型的Class对象。如果你对一个非枚举类型的Class对象调用该方法:

package ch19.values3;

public class Main {
    public static void main(String[] args) {
        System.out.println(Integer.class.getEnumConstants());
    }
}
// null

实现,而非继承

前边说了,枚举类型继承自Enum类,Java也不支持多继承,因此我们不能让枚举类型继承其它类。但是可以让其实现其它接口:

package ch19.interface1;

import ch15.test2.Generator;

enum TrafficLight implements Generator<TrafficLight> {
    RED, GREEN, YELLOW;

    @Override
    public TrafficLight next() {
        switch (this) {
            case RED:
                return GREEN;
            case GREEN:
                return YELLOW;
            case YELLOW:
                return RED;
            default:
                return RED;
        }
    }

}

public class Main {
    public static void main(String[] args) {
        Generator<TrafficLight> gen = TrafficLight.RED;
        StringBuilder sb = new StringBuilder();
        sb.append(gen);
        for (int i = 0; i < 10; i++) {
            gen = gen.next();
            sb.append("=>");
            sb.append(gen);
        }
        System.out.println(sb.toString());
    }
}
// RED=>GREEN=>YELLOW=>RED=>GREEN=>YELLOW=>RED=>GREEN=>YELLOW=>RED=>GREEN

在这个示例中,枚举类型TrafficLight实现了Generator接口,可以看作是一个生成器模式的应用,不过因为TrafficLight也可以看作一个简单的状态机,所以也可以看作是一个状态模式,交通灯可以由next方法从一个状态转换到下一个状态。

关于状态模式,可以阅读设计模式 with Python 10:状态模式 - 魔芋红茶's blog (icexmoon.cn)。

随机选取

经常会有从某个枚举类型中随机选一个枚举值的需求,可以为此编写一个工具方法:

package util;

import java.util.Random;

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

    public static <T extends Enum<T>> T random(Class<T> cls) {
        T[] constants = cls.getEnumConstants();
        return constants[rand.nextInt(constants.length)];
    }

    public static void main(String[] args) {
        enum Color {
            RED, BLUE, YELLOW
        }
        for (int i = 0; i < 5; i++) {
            System.out.print(random(Color.class)+" ");
        }
        System.out.println();
    }
}
// BLUE YELLOW YELLOW BLUE RED

可以在方法内定义枚举,虽然这不常见。这种做法和方法内部类没有本质上的区别。

用接口组织枚举

假设你需要做一个和点餐有关的应用,一份餐点分为主食、甜点和咖啡,你可能会创建类似与下面的类结构:

package ch19.interface2;

class Food {
}

class MainCourse extends Food {
}

class Dessert extends Food {
}

class Coffee extends Food {
}

class BraisedChicken extends MainCourse {
}

class StewedBeef extends MainCourse {
}

class BeefSteak extends MainCourse {
}

class Icecream extends Dessert {
}

class Cookie extends Dessert {
}

class BlackCoffee extends Coffee {
}

class DecafCoffee extends Coffee {
}

public class Main {
    public static void main(String[] args) {

    }
}

在这个示例中,所有餐品都继承自Food,并且被继承关系划分为正餐(MainCourse)、甜点(Dessert)、咖啡(Coffee)。

这又是一个为了举例而举例的例子,这其中可能涉及滥用继承的问题,更常见的做法应当是使用三个容器来分别包含正餐、甜点和咖啡的类型。

应该注意到的是,为了实现这种关系,我们不得不创建了大量的类。如果用枚举来创建,就会简洁很多:

package ch19.interface3;

enum Food {
    BRAISED_CHICKEN, STEWED_BEEF, BEEF_STEEK, ICECREAM, COOKIE, BLACK_COFFEE, DECAF_COFFEE;
}

public class Main {
    public static void main(String[] args) {

    }
}

但这样会带来一个新的问题,如何给这些枚举分类?

要知道我们并不能从枚举创建派生类,原因已经在前边解释过了。但是我们可以用接口来表示多个枚举类型的共同“基类”:

package ch19.interface4;

import java.util.Arrays;

import util.Enums;

interface Food {
}

enum MainCourse implements Food {
    BRAISED_CHICKEN, STEWED_BEEF, BEEF_STEEK
}

enum Dessert implements Food {
    ICECREAM, COOKIE
}

enum Coffee implements Food {
    BLACK_COFFEE, DECAF_COFFEE
}

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            printMeal(getAMeal());
        }
    }

    private static Food[] getAMeal() {
        Food[] meal = new Food[3];
        meal[0] = Enums.random(MainCourse.class);
        meal[1] = Enums.random(Dessert.class);
        meal[2] = Enums.random(Coffee.class);
        return meal;
    }

    private static void printMeal(Food[] meal) {
        System.out.println(Arrays.toString(meal));
    }
}
// [BEEF_STEEK, ICECREAM, DECAF_COFFEE]
// [STEWED_BEEF, ICECREAM, BLACK_COFFEE]
// [STEWED_BEEF, ICECREAM, DECAF_COFFEE]
// [STEWED_BEEF, ICECREAM, BLACK_COFFEE]
// [BEEF_STEEK, ICECREAM, BLACK_COFFEE]

在这个例子中,Food接口没有任何方法,其用途是用于标记一个类型,因此也可以叫做“标记接口”,和之前介绍过的Serializable接口用途类似。

之前我们介绍内部类时,提到过在接口内也可以定义类,同样的,在接口中也可以定义枚举类型。我们可以利用这一点让上边这个例子中的接口和枚举的关系更紧密更一目了然:

package ch19.interface5;

import java.util.Arrays;

import util.Enums;

interface Food {
    enum MainCourse implements Food {
        BRAISED_CHICKEN, STEWED_BEEF, BEEF_STEEK
    }

    enum Dessert implements Food {
        ICECREAM, COOKIE
    }

    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE
    }
}

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            printMeal(getAMeal());
        }
    }

    private static Food[] getAMeal() {
        Food[] meal = new Food[3];
        meal[0] = Enums.random(Food.MainCourse.class);
        meal[1] = Enums.random(Food.Dessert.class);
        meal[2] = Enums.random(Food.Coffee.class);
        return meal;
    }

    private static void printMeal(Food[] meal) {
        System.out.println(Arrays.toString(meal));
    }
}
// [BEEF_STEEK, ICECREAM, DECAF_COFFEE]
// [STEWED_BEEF, ICECREAM, BLACK_COFFEE]
// [STEWED_BEEF, ICECREAM, DECAF_COFFEE]
// [STEWED_BEEF, ICECREAM, BLACK_COFFEE]
// [BEEF_STEEK, ICECREAM, BLACK_COFFEE]

当然这种“更加一目了然”的说法是有前提的:必须掌握Java内部类和枚举的相关概念,否则会更加困惑也说不定。

枚举的枚举

如果你有某种特殊的需要的话,可以创建“枚举的枚举”:

package ch19.interface6;

import java.util.Arrays;

import util.Enums;

interface Food {
    enum MainCourse implements Food {
        BRAISED_CHICKEN, STEWED_BEEF, BEEF_STEEK
    }

    enum Dessert implements Food {
        ICECREAM, COOKIE
    }

    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE
    }
}

enum Course {
    MAIN_COURSE(Food.MainCourse.class), DESSERT(Food.Dessert.class), COFFEE(Food.Coffee.class);

    private Food[] values;

    private Course(Class<? extends Food> cls) {
        values = cls.getEnumConstants();
    }

    public Food random() {
        return Enums.random(values);
    }
}

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            printMeal(getAMeal());
        }
    }

    private static Food[] getAMeal() {
        Food[] meal = new Food[Course.values().length];
        int i = 0;
        for (Course c : Course.values()) {
            meal[i++] = c.random();
        }
        return meal;
    }

    private static void printMeal(Food[] meal) {
        System.out.println(Arrays.toString(meal));
    }
}
// [BEEF_STEEK, ICECREAM, DECAF_COFFEE]
// [STEWED_BEEF, ICECREAM, BLACK_COFFEE]
// [STEWED_BEEF, ICECREAM, DECAF_COFFEE]
// [STEWED_BEEF, ICECREAM, BLACK_COFFEE]
// [BEEF_STEEK, ICECREAM, BLACK_COFFEE]

这里的关键在于,新创建的枚举类型Course中,通过构造函数传递另一个枚举类型的Class对象,这样就可以人为地创建枚举和枚举之间的联系。

还有要注意的一点是,构造函数中的参数类型是Class<? extends Food>,而不是Class<? extends Enum>,这是因为在构造函数中我们需要调用Class.getEnumConstants来获取相关的枚举值,而该方法返回的类型是T[],也就是说Class<? extends Enum>将会返回Enum[],这显然不是我们所希望的。

我们也可以像之前使用内部类一样进一步整合:

package ch19.interface7;

import java.util.Arrays;

import util.Enums;

enum Course {
    MAIN_COURSE(Food.MainCourse.class), DESSERT(Food.Dessert.class), COFFEE(Food.Coffee.class);

    interface Food {
        enum MainCourse implements Food {
            BRAISED_CHICKEN, STEWED_BEEF, BEEF_STEEK
        }

        enum Dessert implements Food {
            ICECREAM, COOKIE
        }

        enum Coffee implements Food {
            BLACK_COFFEE, DECAF_COFFEE
        }
    }

    private Food[] values;

    private Course(Class<? extends Food> cls) {
        values = cls.getEnumConstants();
    }

    public Food random() {
        return Enums.random(values);
    }
}

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            printMeal(getAMeal());
        }
    }

    private static Course.Food[] getAMeal() {
        Course.Food[] meal = new Course.Food[Course.values().length];
        int i = 0;
        for (Course c : Course.values()) {
            meal[i++] = c.random();
        }
        return meal;
    }

    private static void printMeal(Course.Food[] meal) {
        System.out.println(Arrays.toString(meal));
    }
}
// [BEEF_STEEK, ICECREAM, DECAF_COFFEE]
// [STEWED_BEEF, ICECREAM, BLACK_COFFEE]
// [STEWED_BEEF, ICECREAM, DECAF_COFFEE]
// [STEWED_BEEF, ICECREAM, BLACK_COFFEE]
// [BEEF_STEEK, ICECREAM, BLACK_COFFEE]

本质上,这个示例和前边的只是代码组织方式不同。

老实说,这样的代码的确不容易看懂。。。

EnumSet

EnumSet是一个Java为枚举类型“量身定做”的集合,大致上它的使用方式与普通Set相似,不同的是它使用一些特殊的途径对性能做了优化,在查找元素和集合运算等方面速度要优于一般的Set。

package ch19.enum_set;

import java.util.EnumSet;

enum WeekDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

public class Main {
    public static void main(String[] args) {
        EnumSet<WeekDay> weekday = EnumSet.noneOf(WeekDay.class);
        weekday.add(WeekDay.MONDAY);
        System.out.println(weekday);
        weekday.addAll(EnumSet.of(WeekDay.TUESDAY, WeekDay.FRIDAY));
        System.out.println(weekday);
        weekday.addAll(EnumSet.allOf(WeekDay.class));
        System.out.println(weekday);
        weekday.removeAll(EnumSet.range(WeekDay.FRIDAY, WeekDay.SUNDAY));
        System.out.println(weekday);
        weekday.remove(WeekDay.MONDAY);
        System.out.println(weekday);
    }
}
// [MONDAY]
// [MONDAY, TUESDAY, FRIDAY]
// [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
// [MONDAY, TUESDAY, WEDNESDAY, THURSDAY]
// [TUESDAY, WEDNESDAY, THURSDAY]

EnumSet是一个继承自AbstractSet的抽象类,所以不能用new关键字创建,需要使用EnumSet.noneOf、EnumSet.of、EnumSet.allOf等静态方法创建EnumSet实例,这些方法的用途一目了然,这里不做过多解释。

此外,需要注意的是EnumSet实例是一个有序Set,它的遍历顺序是相关联的枚举类型中枚举值的顺序。这其实和EnumSet的底层实现有关,EnumSet使用一个long的bit位来表示某个枚举值是否存在,存在为1,不存在为0。这样做的好处是:

  1. 节省空间,用1个long可以表示64个枚举值。

  2. 判断某个枚举值是否存在,或者进行集合运算时都很快,因为是位运算。

当然也不是没有缺陷,你只能对于一些连续的有限个数据使用类似的方式处理,但显然枚举类型恰好是这样的数据。

比如示例中最后时刻的EnumSet实例的底层存储情况,可以用下图表示:

image-20220410192229689

可能有人会疑惑,如果某个枚举类型有超过64个枚举值,EnumSet要如何处理。事实上EnumSet的确可以正确处理此类的枚举类型,一个可能的猜测是:它会在需要的时候使用额外的long来进行“扩展”。

EnumMap

和EnumSet类似,EnumMap是专门为枚举类型设计的Map,更准确的说,是一种以枚举值作为Key的Map。

在前边学习容器的时候,我们知道,实现Map这类容器的难点在于如何实现一个高效查找、添加删除的Key,最终的实现方式是一个可根据情况扩容的哈希表(Hash Table)。

但如果我们要存储的Key仅限于枚举值,那难度就下降了很多,完全可以使用一个数组来存储,且无需扩容。而EnumMap的底层就是如此做的。

下面是使用EnumMap和枚举类型实现的一个简单日程表程序:

package ch19.enum_map;

import java.util.EnumMap;
import java.util.Map;

interface DayLife {
    void doSomething();
}

enum WeekDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

public class Main {
    public static void main(String[] args) {
        EnumMap<WeekDay, DayLife> weekLife = new EnumMap<>(WeekDay.class);
        initWeekLife(weekLife);
        showWeekLife(weekLife);
        weekLife.put(WeekDay.MONDAY, new DayLife() {
            @Override
            public void doSomething() {
                System.out.println("working...");
            }
        });
        weekLife.put(WeekDay.TUESDAY, new DayLife() {
            @Override
            public void doSomething() {
                System.out.println("shopping...");
            }
        });
        weekLife.put(WeekDay.SUNDAY, new DayLife() {
            @Override
            public void doSomething() {
                System.out.println("go fishing...");
            }
        });
        showWeekLife(weekLife);
    }

    private static void showWeekLife(EnumMap<WeekDay, DayLife> weekLife) {
        System.out.println("============ week life ==========");
        for (Map.Entry<WeekDay, DayLife> entry : weekLife.entrySet()) {
            WeekDay weekDay = entry.getKey();
            DayLife dayLife = entry.getValue();
            if (null != dayLife) {
                System.out.print(weekDay + ": ");
                dayLife.doSomething();
            }
        }
    }

    private static void initWeekLife(EnumMap<WeekDay, DayLife> weekLife) {
        for (WeekDay weekDay : WeekDay.values()) {
            weekLife.put(weekDay, new DayLife() {
                @Override
                public void doSomething() {
                    System.out.println("need do nothing.");
                }
            });
        }
    }
}
// ============ week life ==========
// MONDAY: need do nothing.
// TUESDAY: need do nothing.
// WEDNESDAY: need do nothing.
// THURSDAY: need do nothing.
// FRIDAY: need do nothing.
// SATURDAY: need do nothing.
// SUNDAY: need do nothing.
// ============ week life ==========
// MONDAY: working...
// TUESDAY: shopping...
// WEDNESDAY: need do nothing.
// THURSDAY: need do nothing.
// FRIDAY: need do nothing.
// SATURDAY: need do nothing.
// SUNDAY: go fishing...

这个示例中,使用一个接口DayLife作为EnumMap的Value,表示某个星期需要执行的具体事项。这种方式可看做是一个简单的“命令模式”,即将要执行的操作封装为一个“命令接口”,以进行传递或者执行批量操作。

EnumMap的大部分操作与普通的Map没有太大区别,不过因为其Key是一个有限的枚举类型,所以可以像示例中的那样,在创建后、执行具体业务逻辑之前,对其进行初始化,以确保每个Key-Value都是有效的,这样就避免了后续代码需要不断地检测null值。

常量相关的方法

Java枚举有一个很有意思的特性:允许为枚举值添加方法,以实现每个枚举值具备不同行为的能力。这种方法被称作常量相关的方法(constant-specific methods)。

package ch19.constant_method;

enum WeekDay {
    MONDAY {
        @Override
        public String chinese() {
            return "星期一";
        }
    },
    TUESDAY {
        @Override
        public String chinese() {
            return "星期二";
        }
    },
    WEDNESDAY {
        @Override
        public String chinese() {
            return "星期三";
        }
    },
    THURSDAY {
        @Override
        public String chinese() {
            return "星期四";
        }
    },
    FRIDAY {
        @Override
        public String chinese() {
            return "星期五";
        }
    },
    SATURDAY {
        @Override
        public String chinese() {
            return "星期六";
        }
    },
    SUNDAY {
        @Override
        public String chinese() {
            return "星期天";
        }
    };

    public abstract String chinese();
}

public class Main {
    public static void main(String[] args) {
        for (WeekDay weekDay : WeekDay.values()) {
            System.out.println(weekDay.chinese());
        }
    }
}
// 星期一
// 星期二
// 星期三
// 星期四
// 星期五
// 星期六
// 星期天

要实现“常量相关的方法”,需要在枚举类型中定义一个抽象方法,并在枚举值中实现相应的抽象方法。

这看上去有些怪异,因为在OOP中,抽象方法是由子类(导出类)来实现的,而前边说过,枚举值实际上是一个枚举类型的静态属性,它的类型就是枚举类型,并不存在继承关系。

但不管怎么样,这个特性都挺有用的。下面再看一个更有用的例子:

package ch19.constant_method2;

import java.util.EnumSet;

import ch19.constant_method2.SmartHome.Device;

class SmartHome {
    private EnumSet<Device> devices = EnumSet.noneOf(Device.class);

    public void add(Device device) {
        devices.add(device);
    }

    public void clear() {
        devices.clear();
    }

    public void execute() {
        System.out.println("begin smark home system...");
        for (Device device : devices) {
            device.action();
        }
    }

    enum Device {
        DOOR {
            @Override
            public void action() {
                System.out.println("Open the door.");
            }
        },
        LIGHT {
            @Override
            public void action() {
                System.out.println("Light is on.");
            }
        },
        TV {
            @Override
            public void action() {
                System.out.println("TV is on.");
            }
        },
        PS5 {
            @Override
            public void action() {
                System.out.println("PS5 is on.");
            }
        },
        AIR_CONDITIONER {
            @Override
            public void action() {
                System.out.println("Air conditioner is on, and temperature is 15 degrees Celsius.");
            }
        };

        public abstract void action();
    }
}

public class Main {
    public static void main(String[] args) {
        SmartHome sh = new SmartHome();
        sh.add(Device.LIGHT);
        sh.add(Device.TV);
        sh.add(Device.DOOR);
        sh.execute();
        sh.clear();
        sh.add(Device.PS5);
        sh.add(Device.TV);
        sh.add(Device.AIR_CONDITIONER);
        sh.execute();
    }

}
// begin smark home system...
// Open the door.
// Light is on.
// TV is on.
// begin smark home system...
// TV is on.
// PS5 is on.
// Air conditioner is on, and temperature is 15 degrees Celsius.

在这个例子中,用枚举值定义了一组智能家居产品,并用常量相关的方法定义了一个相关操作。这些枚举值的定义顺序是有意义的,这代表着它们在某些只能场景下的调用顺序。

在最后的测试代码中,用一个EnumSet来表示一组设置好的智能家居产品,execute方法可以遍历EnumSet并执行枚举值对应的常量相关方法。

这样做的好处在于,我们并不需要用户使用全套的智能家居产品,他们可以采购任意的若干件,只要添加到EnumSet中,就可以被执行,并且会严格按照我们定义好的顺序执行。

更妙的是添加到EnumSet中的顺序不影响智能家居设备被执行的顺序。

如果用内部类来实现类似的功能,可能需要这样编写代码:

package ch19.constant_method3;

import java.util.SortedSet;
import java.util.TreeSet;

class SmartHome {
    public interface Actionable {
        void action();
    }

    private static abstract class Device implements Comparable<Device>, Actionable {

        private int order;

        public Device(int order) {
            this.order = order;
        }

        @Override
        public int compareTo(Device o) {
            return Integer.compare(this.order, o.order);
        }

    }

    public static final Device DOOR = new Device(0) {
        public void action() {
            System.out.println("Open the door.");
        }
    };
    public static final Device LIGHT = new Device(1) {
        public void action() {
            System.out.println("Light is on.");
        }
    };
    public static final Device TV = new Device(2) {
        public void action() {
            System.out.println("TV is on.");
        };
    };
    public static final Device PS5 = new Device(3) {
        public void action() {
            System.out.println("PS5 is on.");
        };
    };
    public static final Device AIR_CONDITIONER = new Device(4) {
        public void action() {
            System.out.println("Air conditioner is on, and temperature is 15 degrees Celsius.");
        };
    };

    private SortedSet<Device> devices = new TreeSet<>();

    public void add(Device device) {
        devices.add(device);
    }

    public void clear() {
        devices.clear();
    }

    public void execute() {
        System.out.println("begin smark home system...");
        for (Device device : devices) {
            device.action();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SmartHome sh = new SmartHome();
        sh.add(SmartHome.TV);
        sh.add(SmartHome.LIGHT);
        sh.add(SmartHome.DOOR);
        sh.execute();
        sh.clear();
        sh.add(SmartHome.PS5);
        sh.add(SmartHome.AIR_CONDITIONER);
        sh.add(SmartHome.TV);
        sh.execute();
    }
}
// begin smark home system...
// Open the door.
// Light is on.
// TV is on.
// begin smark home system...
// TV is on.
// PS5 is on.
// Air conditioner is on, and temperature is 15 degrees Celsius.

这里使用静态内部类Device起到之前示例中枚举类的用途,用静态属性来替代之前的枚举值。用一个SortSet代替之前的EnumSet,为了表示执行顺序,让Device实现了Comparable接口。

将Device设置为private是有意为之,之前使用的enum,客户端程序是无法创建新的枚举值的,而内部类要起到类似的作用,只能是声明为private的。

可以看到,使用内部类的方式要稍微麻烦一些。

除了上边这种方式,我们还可以设置一个有“默认行为”的常量相关方法:

package ch19.constant_method4;
...
class SmartHome {
	...
    enum Device {
        ...
        AIR_CONDITIONER {
            @Override
            public void action() {
                System.out.println("Air conditioner is on, and temperature is 15 degrees Celsius.");
            }
        },
        XBOX;

        public void action() {
            System.out.println("The device " + this.name() + " is not define operation.");
        };
    }
}

public class Main {
    public static void main(String[] args) {
        ...
        sh.add(Device.XBOX);
        sh.execute();
    }

}
// begin smark home system...
// Open the door.
// Light is on.
// TV is on.
// begin smark home system...
// TV is on.
// PS5 is on.
// Air conditioner is on, and temperature is 15 degrees Celsius.
// The device XBOX is not define operation.

在这个例子中,常量相关的方法action不是抽象方法,而是提供了一个默认的行为。对应的,枚举值如果没有“覆盖”该方法,就会产生类似的行为(就像示例中新添加的枚举值XBOX那样)。

责任链

在设计模式 with Python 15:其它模式(上) - 魔芋红茶's blog (icexmoon.cn)中我介绍过责任链模式(Chain of Responsibility Pattern),当然也可以叫做职责链模式。

实际上同样可以用枚举来实现责任链模式:

package ch19.chain;

import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import util.Fmt;

class MyFile {
    private String path;

    public MyFile(String path) {
        this.path = path;
    }

    public String getExt() {
        return path.substring(path.lastIndexOf(".") + 1, path.length());
    }
}

interface Classifyable {
    boolean classify(MyFile mf, Set<FileClassify> classifies);
}

class ClassifyManager implements Classifyable {

    @Override
    public boolean classify(MyFile mf, Set<FileClassify> classifies) {
        for (FileClassify fileClassify : FileClassify.values()) {
            if (fileClassify.classify(mf, classifies)) {
                return true;
            }
        }
        return false;
    }

}

enum FileClassify implements Classifyable {
    VEDIO {
        @Override
        public boolean classify(MyFile mf, Set<FileClassify> classifies) {
            Set<String> exts = new HashSet<>();
            exts.addAll(Arrays.asList("mkv", "mp4"));
            if (exts.contains(mf.getExt())) {
                classifies.add(this);
                return true;
            }
            return false;
        }
    },
    PICTURE {
        @Override
        public boolean classify(MyFile mf, Set<FileClassify> classifies) {
            Set<String> exts = new HashSet<>();
            exts.addAll(Arrays.asList("jpg", "jpeg", "png"));
            if (exts.contains(mf.getExt())) {
                classifies.add(this);
                return true;
            }
            return false;
        }
    },
    MUSIC {
        @Override
        public boolean classify(MyFile mf, Set<FileClassify> classifies) {
            Set<String> exts = new HashSet<>();
            exts.addAll(Arrays.asList("mp3"));
            if (exts.contains(mf.getExt())) {
                classifies.add(this);
                return true;
            }
            return false;
        }
    },
    OTHER {
        @Override
        public boolean classify(MyFile mf, Set<FileClassify> classifies) {
            classifies.add(this);
            return true;
        }
    };

    @Override
    public abstract boolean classify(MyFile mf, Set<FileClassify> classifies);
}

public class Main {
    public static void main(String[] args) {
        File root = new File("F:\\download");
        ClassifyManager cm = new ClassifyManager();
        for (File file : root.listFiles()) {
            if (file.isFile()) {
                MyFile mf = new MyFile(file.getAbsolutePath());
                Set<FileClassify> classifies = new HashSet<>();
                if (cm.classify(mf, classifies)) {
                    Fmt.printf("file %s classifies:%s\n", file.getName(), classifies.toString());
                }
            }
        }
    }
}

这里用枚举的方式实现了设计模式 with Python 15:其它模式(上) - 魔芋红茶's blog (icexmoon.cn)中的示例。

可以看到,使用枚举类型和常量相关的方法,的确可以减少类的使用,但缺点在于因为枚举值并不算是真正意义上的“类”,所以存在某些限制,比如在上面这个示例中,各枚举值classify方法中的exts本应被设置为属性或静态属性,但因为枚举的关系,并不能这样做,所以只能被设置为方法的局部变量,这样就需要每次调用时对其进行初始化,这本是不必要的。

这个例子很有意思,可以对它进行修改,编写一个帮助自己整理文件夹的小工具。比如对下载目录按照文件类型进行分类。

状态模式

在设计模式 with Python 10:状态模式 - 魔芋红茶's blog (icexmoon.cn)中我介绍过状态模式,使用枚举同样可以实现状态模式。

这里依然通过实现原文中的饮料机的方式作为示例:

package ch19.status_machine;

import static ch19.status_machine.Status.*;

import java.util.Random;

import ch15.test2.Generator;
import util.Fmt;

//饮料机的输入
enum Input {
    IN_COIN, OUT_COIN, TURN_BUTTON, IN_DRINK;
}

//饮料机状态
enum Status {
    READY_COIN {
        @Override
        public Status next(Input input, DrinkMachine dm) {
            if (input == Input.IN_COIN) {
                dm.setCoins(dm.getCoins() + 1);
                return AREADY_COIN;
            }
            System.out.println("no support this operation.");
            return this;
        }
    },
    AREADY_COIN {
        @Override
        public Status next(Input input, DrinkMachine dm) {
            if (input == Input.OUT_COIN) {
                dm.setCoins(dm.getCoins() - 1);
                return READY_COIN;
            } else if (input == Input.TURN_BUTTON) {
                return OUT_DRINK;
            }
            System.out.println("no support this operation.");
            return this;
        }
    },
    OUT_DRINK {
        @Override
        public Status next(DrinkMachine dm) {
            dm.setDrinks(dm.getDrinks() - 1);
            if (dm.getDrinks() <= 0) {
                return NO_DRINK;
            } else {
                return READY_COIN;
            }
        }
    },
    NO_DRINK {
        @Override
        public Status next(Input input, DrinkMachine dm) {
            if (input == Input.IN_DRINK) {
                dm.setDrinks(20);
                return READY_COIN;
            }
            System.out.println("no support this operation.");
            return this;
        }
    };

    //判断是否瞬时状态
    public boolean isTrasientStatus() {
        if (StatusCategory.getCategory(this) == StatusCategory.TRASIENT) {
            return true;
        }
        return false;
    }

    //一般状态的状态流转(需要输入)
    public Status next(Input input, DrinkMachine dm) {
        if (this.isTrasientStatus()) {
            throw new RuntimeException("only used for normal status.");
        }
        return null;
    }

    //瞬时状态的状态流转(不需要输入)
    public Status next(DrinkMachine dm) {
        if (!this.isTrasientStatus()) {
            throw new RuntimeException("only used for trasient status.");
        }
        return null;
    }
}

//状态分类
enum StatusCategory {
    //一般状态
    NOMAL(READY_COIN, AREADY_COIN, NO_DRINK),
    //瞬时状态
    TRASIENT(OUT_DRINK);

    private Status[] statuses;

    private StatusCategory(Status... statuses) {
        this.statuses = statuses;
    }

    public static StatusCategory getCategory(Status status) {
        for (StatusCategory sc : StatusCategory.values()) {
            for (Status s : sc.statuses) {
                if (s == status) {
                    return sc;
                }
            }
        }
        return null;
    }
}

class DrinkMachine {
    private int drinks = 0;
    private int coins = 0;
    private Status status = NO_DRINK;

    public DrinkMachine() {
        this.operate(Input.IN_DRINK);
    }

    public void operate(Input input) {
        Status newStatus = this.status.next(input, this);
        this.status = newStatus;
        System.out.println(this);
        // 如果新状态是一个瞬时状态,继续运行,直到一个非瞬时状态
        while (newStatus.isTrasientStatus()) {
            newStatus = newStatus.next(this);
            this.status = newStatus;
            System.out.println(this);
        }
    }

    public int getDrinks() {
        return drinks;
    }

    public int getCoins() {
        return coins;
    }

    public void setCoins(int coins) {
        this.coins = coins;
    }

    public void setDrinks(int drinks) {
        this.drinks = drinks;
    }

    @Override
    public String toString() {
        return Fmt.sprintf("now drink machine: drinks(%d), coins(%d), status(%s).", this.drinks, this.coins,
                this.status);
    }
}

class RandomInput implements Generator<Input> {
    private static Random rand = new Random();

    @Override
    public Input next() {
        Input[] inputs = Input.values();
        return inputs[rand.nextInt(inputs.length)];
    }

}

public class Main {
    public static void main(String[] args) {
        DrinkMachine dm = new DrinkMachine();
        RandomInput ri = new RandomInput();
        for (int i = 0; i < 20; i++) {
            Input input = ri.next();
            Fmt.printf("now do operate %s\n", input);
            dm.operate(input);
        }
    }
}

为了方便理解,我在关键位置添加了注释。

这个示例和设计模式 with Python 10:状态模式 - 魔芋红茶's blog (icexmoon.cn)中用Python实现的示例最大的区别在于,将对饮料机的操作封装为了Input枚举类型,这样做的好处在于可以用单一的next方法表示不同状态之间的流转,而不是像Python代码那样每个状态需要定义多个方法来对应多种操作,如果要添加新的操作就需要给已有的每个状态添加一个新方法。

将操作封装为枚举类型的一个额外好处是,可以很容易地实现一个随机产生操作并对饮料机进行测试的程序。

此外,参考《Java编程思想》中的示例,这里同样引入了“瞬时状态”的概念,即一个状态流转的中间状态,该状态并不需要外部输入,只需要依据内部条件进行流转。因此添加了一个额外枚举StatusCategory来区分普通状态和中间状态。相应的,状态流转方法next也使用两种形式,需要Input参数的用于普通状态,不需要Input参数的用于瞬时状态。

最后用DrinkMachine表示饮料机,并提供一个operate方法用于操作饮料机。每次操作饮料机时,饮料机必然会停留在一个普通状态,此时通过operate操作饮料机,会根据当前状态使用状态的next方法进行状态流转,如果下一个状态是一个瞬时状态,就继续流转,直到饮料机停留在一个普通状态。

这里的示例可以看做是《Java编程思想》中原始示例的简化版本,原始示例的输入还包含饮料选择等其他内容,同样很有借鉴价值,感兴趣的可以阅读原文。

多路分发

多路分发是一个难以理解的概念,这里找到一篇不错的文章java单分派与多分派(多路分发和单路分发) - - ITeye博客,该文章清除地阐述了多路分发和单路分发的区别,以及Java为什么是单路分发。

这里看一个经典的石头剪刀布问题:

package ch19.finger_game;

import java.util.Random;

import ch15.test2.Generator;
import util.Fmt;

abstract class Item {
    public abstract Result compete(Rock rock);

    public abstract Result compete(Scissors scissors);

    public abstract Result compete(Paper paper);

    @Override
    public String toString() {
        return this.getClass().getSimpleName();
    }
}

class Rock extends Item {

    @Override
    public Result compete(Rock rock) {
        return Result.DRAW;
    }

    @Override
    public Result compete(Scissors scissors) {
        return Result.WIN;
    }

    @Override
    public Result compete(Paper paper) {
        return Result.LOSE;
    }

}

class Scissors extends Item {

    @Override
    public Result compete(Rock rock) {
        return Result.LOSE;
    }

    @Override
    public Result compete(Scissors scissors) {
        return Result.DRAW;
    }

    @Override
    public Result compete(Paper paper) {
        return Result.WIN;
    }
}

class Paper extends Item {

    @Override
    public Result compete(Rock rock) {
        return Result.WIN;
    }

    @Override
    public Result compete(Scissors scissors) {
        return Result.LOSE;
    }

    @Override
    public Result compete(Paper paper) {
        return Result.DRAW;
    }
}

enum Result {
    WIN, LOSE, DRAW
}

class RandomItem implements Generator<Item> {
    private static Item[] items = new Item[] { new Rock(), new Paper(), new Scissors() };
    private static Random rand = new Random();

    @Override
    public Item next() {
        return items[rand.nextInt(items.length)];
    }

}

public class Main {
    public static void main(String[] args) {
        RandomItem ri = new RandomItem();
        for (int i = 0; i < 3; i++) {
            Item item = ri.next();
            competeTest(item);
        }
    }

    private static void competeTest(Item item) {
        Paper paper = new Paper();
        Result r = item.compete(paper);
        Fmt.printf("%s vs %s = %s\n", item, paper, r);
        Rock rock = new Rock();
        r = item.compete(rock);
        Fmt.printf("%s vs %s = %s\n", item, rock, r);
        Scissors scissors = new Scissors();
        r = item.compete(scissors);
        Fmt.printf("%s vs %s = %s\n", item, scissors, r);
    }

}

这里用Paper、Rock、Scissors分别代表布、石头和剪刀,并且它们具有相同的基类Item。

为了能让它们互相之间比较,这里使用了重载的compete方法。

似乎一切都很好,但如果你仔细观察测试程序,就能发现并不是很好用。虽然方法的接收方可以用RandomItem生成一个随机的Item对象,但是compete方法必须要指定一个具体的Item类型,不能同样使用一个随机生成的Item对象,这里的深层次原因就是Java不支持多路分发,只支持单路分发。也就是说compete方法在调用时,仅会根据参数的“句柄类型”来确定使用何种方法调用,而非其真实类型。

要改变这种现状也不难,使用反射获取参数的真实类型并作出相应处理即可:

package ch19.finger_game2;

import java.util.Random;

import ch15.test2.Generator;
import util.Fmt;

abstract class Item {
	...
    public Result compete(Item item) {
        if (item instanceof Rock) {
            Rock rock = (Rock) item;
            return this.compete(rock);
        } else if (item instanceof Scissors) {
            Scissors scissors = (Scissors) item;
            return this.compete(scissors);
        } else if (item instanceof Paper) {
            Paper paper = (Paper) item;
            return this.compete(paper);
        } else {
            return null;
        }
    }
	...
}
...
public class Main {
    public static void main(String[] args) {
        RandomItem ri = new RandomItem();
        for (int i = 0; i < 10; i++) {
            Item item1 = ri.next();
            Item item2 = ri.next();
            vs(item1, item2);
        }
    }

    private static void vs(Item item1, Item item2) {
        Result r = item1.compete(item2);
        Fmt.printf("%s vs %s = %s\n", item1, item2, r);
    }

}
// Paper vs Scissors = LOSE
// Rock vs Scissors = WIN
// Rock vs Paper = LOSE
// Scissors vs Paper = WIN
// Scissors vs Paper = WIN
// Scissors vs Scissors = DRAW
// Rock vs Scissors = WIN
// Scissors vs Paper = WIN
// Paper vs Rock = WIN

只需要在基类Item中添加一个compete(Item item)方法,并在其中使用反射来根据真实类型调用不同的compete方法即可。实际上这个compete(Item item)方法所做的事情就是二次分发,也可以叫做二路分发。

虽然这里已经实现了多路分发,但问题在于这种if...else结构不利于代码维护,如果我们后边引入了其它类型的Item,就需要修改这里的if...else,这显然是一种糟糕的设计。

实际上我们可以利用Java的单路分发机制,来巧妙地避免使用反射,相应的也就可以避免if...else结构:

package ch19.finger_game3;

import java.util.Random;

import ch15.test2.Generator;
import util.Fmt;

abstract class Item {
	...
    @Override
    public String toString() {
        return this.getClass().getSimpleName();
    }
}

class Rock extends Item {
	...
    @Override
    public Result compete(Item item) {
        return item.compete(this).reverse();
    }

}

class Scissors extends Item {
	...
    @Override
    public Result compete(Item item) {
        return item.compete(this).reverse();
    }
}

class Paper extends Item {
	...
    @Override
    public Result compete(Item item) {
        return item.compete(this).reverse();
    }
}

enum Result {
    WIN, LOSE, DRAW;

    public Result reverse() {
        switch (this) {
            case WIN:
                return LOSE;
            case LOSE:
                return WIN;
            case DRAW:
                return DRAW;
            default:
                return null;
        }
    }
}
...

这里的关键在于,在Item的具体子类的compete(Item item)方法中,使用item.compete(this)的方式进行二次分发,将方法调用分发给具体的compete方法。其原理是:经过第一次的多态调用分发,compete方法的接收者的类型是确定的,也就是说this的静态类型是可以确定的,即当前的class类,自然就可以作为参数的“句柄类型”进行二次分发。

这种方式看上去就像是方法的接收方和参数调换了位置,因此也被叫做“反转球”。

还有一些细节需要注意:

  1. 二次分发只能发生在item的具体子类,你不能在Item的compete(Item item)方法中完成二次分发,因为Item类中的this只能确定是一个Item类型,其真实类型是Item的哪个子类必须在运行时才能知道,静态编译期是无法知道的,也就无法作为参数类型进行分发。

  2. 虽然我们通过二次分发确实调用到了具体的compete方法,但是比较本身是有序的,比如原本我们比较的是石头和剪刀,但是经过这种方式二次分发后,实际上比较的是剪刀和石头,这样就会产生错误结果。因此这里为Result枚举编写了一个方法reverse,用于将结果反转,以获取正确结果。

可能有人会觉得这里同样引入了额外的if...else语句(枚举类型中的reverse方法)。实际上可以通过给Item添加一个isCompeted方法(与compete的比较顺序相反),并在二次分发时改为调用isCompeted即可完全避免引入if...else,但需要修改的代码也会增多,我个人觉得不是很有必要。

enum 实现多路分发

下面是一个用枚举实现的石头剪刀布的版本:

package ch19.finger_game4;

import java.util.Random;

import ch15.test2.Generator;
import util.Fmt;

enum Result {
    WIN, LOSE, DRAW
}

enum Item {
    PAPER(Result.DRAW, Result.WIN, Result.LOSE),
    ROCK(Result.LOSE, Result.DRAW, Result.WIN),
    SCISSORS(Result.WIN, Result.LOSE, Result.DRAW);

    private Result vsPaper;
    private Result vsRock;
    private Result vsScissors;

    private Item(Result vsPaper, Result vsRock, Result vsScissors) {
        this.vsPaper = vsPaper;
        this.vsRock = vsRock;
        this.vsScissors = vsScissors;
    }

    public Result compete(Item item) {
        switch (item) {
            case PAPER:
                return this.vsPaper;
            case ROCK:
                return this.vsRock;
            case SCISSORS:
                return this.vsScissors;
            default:
                return null;
        }
    }
}

class RandomItem implements Generator<Item> {
    private static Random rand = new Random();

    @Override
    public Item next() {
        Item[] items = Item.values();
        return items[rand.nextInt(items.length)];
    }

}

public class Main {
    public static void main(String[] args) {
        RandomItem ri = new RandomItem();
        for (int i = 0; i < 10; i++) {
            vs(ri.next(), ri.next());
        }
    }

    private static void vs(Item item1, Item item2) {
        Result r = item1.compete(item2);
        Fmt.printf("%s vs %s = %s\n", item1, item2, r);
    }
}

在这个示例中,通过构造函数和vsPaper等属性,每个Item枚举值保存了对其它枚举值的胜负关系,因此只要在compete方法中通过switch判断出被比较的Item对象是哪一个枚举值,就可以直接将相应的胜负关系作为结果。实际上这种初始化的胜负关系可以看做是一种“表结构”的数据,通过查表我们可以完成比较。

这种方式也被称作“表驱动式编码”。

在上面这个过程中,实际上switch语句可以看做是在进行“二次分发”。

使用常量相关方法

除了这种方式,还可以利用常量相关的方法进行二次分发:

package ch19.finger_game5;

import java.util.Random;

import ch15.test2.Generator;
import util.Fmt;

enum Result {
    WIN, LOSE, DRAW
}

enum Item {
    PAPER {
        @Override
        public Result compete(Item item) {
            switch (item) {
                case PAPER:
                    return Result.DRAW;
                case ROCK:
                    return Result.WIN;
                case SCISSORS:
                    return Result.LOSE;
                default:
                    return null;
            }
        }
    },
    ROCK {
        @Override
        public Result compete(Item item) {
            switch (item) {
                case PAPER:
                    return Result.LOSE;
                case ROCK:
                    return Result.DRAW;
                case SCISSORS:
                    return Result.WIN;
                default:
                    return null;
            }
        }
    },
    SCISSORS {
        @Override
        public Result compete(Item item) {
            switch (item) {
                case PAPER:
                    return Result.WIN;
                case ROCK:
                    return Result.LOSE;
                case SCISSORS:
                    return Result.DRAW;
                default:
                    return null;
            }
        }
    };

    public abstract Result compete(Item item);
}
...

这种方式的缺点在于每个枚举值的compete方法内容比较相似,但又很难重用,且如果需要添加新的类型,就不得不用类似的方式实现新的相关的compete方法。

《Java编程思想》给出了一种简化方式:

package ch19.finger_game6;

import java.util.Random;

import ch15.test2.Generator;
import util.Fmt;

enum Result {
    WIN, LOSE, DRAW
}

enum Item {
    PAPER {
        @Override
        public Result compete(Item item) {
            return compete(ROCK, item);
        }
    },
    ROCK {
        @Override
        public Result compete(Item item) {
            return compete(SCISSORS, item);
        }
    },
    SCISSORS {
        @Override
        public Result compete(Item item) {
            return compete(PAPER, item);
        }
    };

    public abstract Result compete(Item item);

    protected Result compete(Item loser, Item item) {
        return item == this ? Result.DRAW : (item == loser ? Result.WIN : Result.LOSE);
    }
}
...

其实两者没有本质上的区别,只不过这个版本通过一个compete(Item loser, Item item)方法进行二次转发,并且因为经过一次转发,接收方已经确定,所以可以在二次转发时确定稳赢的比较对象是什么,然后作为二次转发的参数传入,这样就可以在二次转发的compete方法中进行排除来判断结果。

这种方式的缺点在于代码难以阅读。

使用 EnumMap

可以很容易联想到使用EnumMap对之前某个使用“表数据”和switch语句的版本进行改造,以避免switch语句的使用:

...
enum Item {
    PAPER(Result.DRAW, Result.WIN, Result.LOSE),
    ROCK(Result.LOSE, Result.DRAW, Result.WIN),
    SCISSORS(Result.WIN, Result.LOSE, Result.DRAW);

    private EnumMap<Item, Result> table;

    private Item(Result vsPaper, Result vsRock, Result vsScissors) {
        // table.put(PAPER, vsPaper);
        // table.put(ROCK, vsRock);
        // table.put(SCISSORS, vsScissors);
    }

    public Result compete(Item item) {
        return table.get(item);
    }
}
...

实际上上面的代码无法通过编译,因为被注释掉的部分陷入了“先有蛋还是先有鸡”的问题,在枚举类型的构造函数没有完成调用前,枚举值是不可用的,然而我们又要在构造函数中使用枚举值。这显然是不可行的。

但是我们可以更进一步,使用EnumMap来取代所有的分发,即连第一步枚举方法的分发也可以由EnumMap来实现:

...
enum Item {
    PAPER,
    ROCK,
    SCISSORS;

    private static EnumMap<Item, EnumMap<Item, Result>> table = new EnumMap<>(Item.class);

    static {
        EnumMap<Item, Result> paperRow = new EnumMap<>(Item.class);
        EnumMap<Item, Result> rockRow = new EnumMap<>(Item.class);
        EnumMap<Item, Result> scissorsRow = new EnumMap<>(Item.class);
        table.put(PAPER, paperRow);
        table.put(ROCK, rockRow);
        table.put(SCISSORS, scissorsRow);
        paperRow.put(PAPER, Result.DRAW);
        paperRow.put(ROCK, Result.WIN);
        paperRow.put(SCISSORS, Result.LOSE);
        rockRow.put(PAPER, Result.LOSE);
        rockRow.put(SCISSORS, Result.WIN);
        rockRow.put(ROCK, Result.DRAW);
        scissorsRow.put(SCISSORS, Result.DRAW);
        scissorsRow.put(ROCK, Result.LOSE);
        scissorsRow.put(PAPER, Result.WIN);
    }

    public Result compete(Item item) {
        return table.get(this).get(item);
    }

}
...

使用二维数组

实现表数据的方式可以是多种多样的,实际上对于其它语言的开发者而言,使用二维数组应该是最直观的想法。

但障碍在于二维数组只能用整形表示两组相关的数据,但枚举实际上是可以很容易地在整形与枚举之间建立映射关系。因此我们可以用二维数组的方式实现分发:

...
enum Item {
    PAPER,
    ROCK,
    SCISSORS;

    private static final int SIZE = Item.values().length;

    private static Result[][] table = new Result[SIZE][];

    static {
        table[PAPER.ordinal()] = new Result[SIZE];
        table[ROCK.ordinal()] = new Result[SIZE];
        table[SCISSORS.ordinal()] = new Result[SIZE];
        Result[] paperRow = table[PAPER.ordinal()];
        paperRow[PAPER.ordinal()] = Result.DRAW;
        paperRow[SCISSORS.ordinal()] = Result.LOSE;
        paperRow[ROCK.ordinal()] = Result.WIN;
        Result[] rockRow = table[ROCK.ordinal()];
        rockRow[ROCK.ordinal()] = Result.DRAW;
        rockRow[PAPER.ordinal()] = Result.LOSE;
        rockRow[SCISSORS.ordinal()] = Result.WIN;
        Result[] scissorsRow = table[SCISSORS.ordinal()];
        scissorsRow[SCISSORS.ordinal()] = Result.DRAW;
        scissorsRow[PAPER.ordinal()] = Result.WIN;
        scissorsRow[ROCK.ordinal()] = Result.LOSE;
    }

    public Result compete(Item item) {
        return table[this.ordinal()][item.ordinal()];
    }

}
...

可能会有人觉得示例中数组初始化的方式太过麻烦,实际上可以用下边的方式初始化:

package ch19.finger_game10;
...
enum Item {
    PAPER,
    ROCK,
    SCISSORS;

    private static Result[][] table = new Result[][] {
            { Result.DRAW, Result.WIN, Result.LOSE },
            { Result.LOSE, Result.DRAW, Result.WIN },
            { Result.WIN, Result.LOSE, Result.DRAW } };

    public Result compete(Item item) {
        return table[this.ordinal()][item.ordinal()];
    }

}
...

这种方式简洁很多,但是需要注意的是,在初始化数组时需要严格按照枚举值对应的整数编写字面量语句。且这种代码存在维护性差的问题,如果你要调整枚举值的定义顺序,就必须重写二维数组的初始化字面量语句。

本来以为这会是简单的一个章节,没想到依然有这么多内容,无论如何,总算是写完了,谢谢阅读。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 枚举
最后更新:2022年4月13日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号