Java编程思想笔记0x12

枚举类型(二)

使用EnumSet替代标志

  • 是一种面向enum的标志,可以用来表示某种“开/关”信息。其内部实现是将一个long值作为位向量,因此EnumSet非常高效。
enum AlarmPoints {
    STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
    OFFICE4, BATHROOM, UTILITY, KITCHEN
}

public class Test {
    public static void main(String[] args) throws Exception {
        EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class);
        points.add(AlarmPoints.BATHROOM);
        System.out.println(points);
        points.addAll(EnumSet.of(AlarmPoints.STAIR1, AlarmPoints.STAIR2, AlarmPoints.KITCHEN));
        System.out.println(points);
        points = EnumSet.allOf(AlarmPoints.class);
        points.removeAll(EnumSet.of(AlarmPoints.STAIR1, AlarmPoints.STAIR2, AlarmPoints.KITCHEN));
        System.out.println(points);
        points.removeAll(EnumSet.range(AlarmPoints.OFFICE1, AlarmPoints.OFFICE4));
        System.out.println(points);
        points = EnumSet.complementOf(points);
        System.out.println(points);
    }
}
  • EnumSet.of()方法被重载了很多次,不仅为可变数量参数进行类重载,还为接收1到5个显式的参数的情况进行类重载。即当使用2到5个参数时,会调用对应的重载方法,而使用1个或者多于5个参数时,会调用可变参数的of()方法。注意,1个参数不会导致编译器构造可变参数数组。
  • 在JDK 11中,EnumSet.of()不同的重载方法有:1至5个显式声明参数的方法,和一个参数列表为(E first, E... rest)的方法。

  • 在创建EnumSet对象时,如果枚举实例小于等于64个,则使用一个long值,否则使用一个long数组。

使用EnumMap

  • EnumMap要求其中的键必须来自一个enum。由于枚举实例总数量是确定的,EnumMap内部使用了数组实现。调用put()方法时键只能是枚举实例,其它操作和普通的Map基本一致。
enum AlarmPoints {
    STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
    OFFICE4, BATHROOM, UTILITY, KITCHEN
}
interface Command {
    void action();
}

public class Test {
    public static void main(String[] args) throws Exception {
        EnumMap<AlarmPoints, Command> em = new EnumMap<>(AlarmPoints.class);
        em.put(AlarmPoints.KITCHEN, new Command() {
            public void action() {
                System.out.println("Kichen fire!");
            }
        });
        em.put(AlarmPoints.BATHROOM, new Command() {
            public void action() {
                System.out.println("Bathroom alart!");
            }
        });
        for(Map.Entry<AlarmPoints, Command> e: em.entrySet()) {
            System.out.print(e.getKey() + ": ");
            e.getValue().action();
        }
        try {
            em.get(AlarmPoints.UTILITY).action();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}
/* Output:
BATHROOM: Bathroom alart!
KITCHEN: Kichen fire!
java.lang.NullPointerException
*/
  • 上面代码是一种命令模式的用法,即首先需要一个只有单一方法的接口,然后从该接口实现具有各自不同的行为的多个子类。之后可以根据需要构造命令对象并使用。
  • 如果EnumMap没有为某个键调用put()方法来存入相应的值的话,那么其对应的值就为null
  • 注意foreach中元素的泛型声明不可省略

常量相关方法

  • 可以为enum实例编写方法,从而为每个枚举实例赋予各自不同的行为。首先需要为enum定义一个或多个方法(或者抽象方法),然后为选择枚举实例进行覆盖方法(如果是抽象方法则每个枚举实例都要实现该方法)。
public enum Test {
    DATE_TIME {
        String getInfo() {
            return DateFormat.getDateInstance().format(new Date());
        }
    }, 
    CLASSPATH {
        String getInfo() {
            return System.getenv("CLASSPATH");
        }
    }, 
    VERSION {
        String getInfo() {
            return System.getProperty("java.version");
        }
    },
    TEST {
        String getInfo() {
            return "test info";
        }
        int getNumber() {
            return 222222;
        }
    };
    abstract String getInfo();
    int getNumber() {
        return 111111;
    }
    public static void main(String[] args) throws Exception {
        for (Test t: values()) {
            System.out.println(t.getInfo() + " " + t.getNumber());
        }
    }
}
/* Output:
2019年7月21日 111111
.;C:\Program Files\Java\jdk-11.0.3\lib\dt.jar;C:\Program Files\Java\jdk-11.0.3\lib\tools.jar; 111111
11.0.3 111111
test info 222222
*/
  • 通过相应的枚举实例,可以调用其上的方法,这通常也称为表驱动的代码(tabledriven code)。在上面代码中,枚举实例似乎被当做其“超类”来使用,在调用覆盖(实现)的方法(抽象方法)时体现多态的行为。

使用enum职责链

  • 在职责链设计模式中,可以用多种不同的方法来解决一个问题,然后将它们链接在一起。当一个请求到来时,遍历这个链,直到链中的某个解决方案能够处理该请求。
  • 通过常量相关的方法,可以很容易地实现一个简单的职责链。其中每一次尝试可以看做一个策略(设计模式),完整的处理方式列表就是一个职责链。
enum Handle {
    PLUS {
        boolean modify(int src, int target) {
            switch(target - src) {
                case 1: 
                    System.out.println("plus");
                    return true;
                default: return false;
            }
        }
    },
    MINUS {
        boolean modify(int src, int target) {
            switch(target - src) {
                case -1: 
                    System.out.println("minus");
                    return true;
                default: return false;
            }
        }
    },
    ABS {
        boolean modify(int src, int target) {
            switch(target + src) {
                case 0: 
                    System.out.println("abs");
                    return true;
                default: return false;
            }
        }
    };
    abstract boolean modify(int src, int target);
} 
public class Test {
    static void modify(int src, int target) {
        System.out.print(src + " ");
        for(Handle h: Handle.values()) {
            if(h.modify(src, target)) return;
        }
        System.out.println("can't modify");
    }
    public static void main(String[] args) throws Exception {
        Random r = new Random(47);
        int target = r.nextInt(10) - 5;
        System.out.println(target);
        for (int i = 0; i < 10; i++) {
            int src = r.nextInt(10) - 5;
            modify(src, target);
        }
    }
}
/* Output:
3
0 can't modify
-2 can't modify
-4 can't modify
-4 can't modify
4 minus
3 can't modify
-5 can't modify
-3 abs
2 plus
3 can't modify
*/

使用enum的状态机

  • 枚举类型非常适合用来创建状态机。一个状态机有有限的特定状态,它通常根据输入,从一个状态转移到下一个状态。每个状态都具有某些可接受的输入,不同的输入回事状态机从当前状态转移到不同的新状态。由于enum对其实例有严格的限制,非常适合用来表现不同的状态和输入。
enum Check {
    EVEN {
        void next(char n) {
            switch(n) {
                case '0':
                    c = ODD;
                    return;
                case '1':
                    return;
                default:
                    c = ERROR;
            }
        }
    },
    ODD {
        void next(char n) {
            switch(n) {
                case '0':
                    c = EVEN;
                    return;
                case '1':
                    return;
                default:
                    c = ERROR;
            }
        }
    },
    ERROR;
    private static Check c;
    void next(char n) { throw new RuntimeException("no such method."); };
    public static void check(String num) {
        c = EVEN;
        char[] ns = num.toCharArray();
        for (char n: ns) {
            c.next(n);
            if(c == ERROR) break;
        }
        switch(c) {
            case ODD:
                System.out.println("odd 0s");
                return;
            case EVEN:
                System.out.println("even 0s");
                return;
            case ERROR:
                System.out.println("incorrect input");
        }
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        Check.check("01001100001");
        Check.check("000011110000");
        Check.check("00110020");
    }
}
/* Output:
odd 0s
even 0s
incorrect input
*/
  • 上面代码中实现了判断一串字符串的二进制数中有奇数还是偶数个0。有三种状态,EVENODDERROR,前两种状态根据输入的不同决定将要转移的下一个状态,而ERROR则表示输入有误,仅作为最终状态。

多路分发

  • 如果存在一种多元运算,其变量类型范围确定但是不能知道某个变量的具体类型,并且该运算结果与变量类型相关,即该运算的实现需要判断各个参数的类型,那么此时可以使用多路分发。
enum Outcome { WIN, DRAW, LOSE}
interface Item {
    Outcome compete(Item it);
    Outcome eval(Paper p);
    Outcome eval(Scissors s);
    Outcome eval(Rock r);
}
class Paper implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return Outcome.DRAW; }
    public Outcome eval(Scissors s) { return Outcome.WIN; }
    public Outcome eval(Rock r) { return Outcome.LOSE; }
    public String toString() { return "Paper"; }
}
class Scissors implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return Outcome.LOSE; }
    public Outcome eval(Scissors s) { return Outcome.DRAW; }
    public Outcome eval(Rock r) { return Outcome.WIN; }
    public String toString() { return "Scissors"; }
}
class Rock implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return Outcome.WIN; }
    public Outcome eval(Scissors s) { return Outcome.LOSE; }
    public Outcome eval(Rock r) { return Outcome.DRAW; }
    public String toString() { return "Rock"; }
}
public class Test {
    static final int SIZE = 10;
    private static Random r = new Random(47);
    public static Item newItem() {
        switch(r.nextInt(3)) {
            default:
            case 0: return new Scissors();
            case 1: return new Rock();
            case 2: return new Paper();
        }
    }
    public static void match(Item a, Item b) {
        System.out.println(a + " vs. " + b + ": " + a.compete(b));
    }
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < SIZE; i++) {
            match(newItem(), newItem());
        }
    }
}
  • 上面代码实现了“石头、剪刀、布”游戏,采用了接口的方式进行多路分发(两路分发),避免了需要判断类型的代码。

使用enum分发

enum Outcome { WIN, DRAW, LOSE}
enum RoShamBo2 {
    PAPER(Outcome.DRAW, Outcome.LOSE, Outcome.WIN),
    SCISSORS(Outcome.WIN, Outcome.DRAW, Outcome.LOSE),
    ROCK(Outcome.LOSE, Outcome.WIN, Outcome.DRAW);
    private Outcome vPaper, vScissors, vRock;
    RoShamBo2(Outcome paper, Outcome scissors, Outcome rock) {
        this.vPaper = paper;
        this.vScissorc = scissors;
        this.vRock = rock;
    }
    public Outcome compete(RoShamBo2 it) {
        switch(it) {
            default:
            case Outcome.PAPER: return vPaper;
            case Outcome.SCISSORS: return vScissors;
            case Outcome.ROCK: return vRock;
        }
    }
}
// 调用略
  • 上面代码使用构造器来初始化每个枚举实例,并以一组结果作为参数,形成类类似查询表的结构。

使用常量相关的方法

  • 为每个枚举实例提供方法的不同实现同样可以完成多路分发,但是在需要添加类型时代码的改动量要多于使用enum分发。
enum Outcome { WIN, DRAW, LOSE}
enum RoShamBo3 {
    PAPER {
        public Outcome compete(RoShamBo3 it) {
            switch(it) {
                default: 
                case PAPER: return DRAW;
                case SCISSORS: return LOSE;
                case ROCK: return WIN;
            }
        }
    };
    // SCISSORS, ROCK略 
}

使用EnumMap分发

enum Outcome { WIN, DRAW, LOSE}
enum RoShamBo5 {
    PAPER, SCISSORS, ROCK;
    static EnumMap<RoShamBo5, EnumMap<RoShamBo5, Outcome>> table = new EnumMap<>(RoShamBo5.class);
    static {
        for (RoShamBo5 it: RoShamBo5.values()) {
            table.put(it, new EnumMap<>(RoShamBo5.class));
        }
        initRow(PAPER, Outcome.DRAW, Outcome.LOSE, Outcome.WIN);
        initRow(SCISSORS, Outcome.WIN, Outcome.DRAW, Outcome.LOSE);
        initRow(ROCK, Outcome.LOSE, Outcome.WIN, Outcome.DRAW);
    }
    static void initRow(RoShamBo5 it, Outcome vPAPER, Outcome vSCISSORS, Outcome vROCK) {
        EnumMap<RoShamBo5, Outcome> row = RoShamBo5.table.get(it);
        row.put(RoShamBo5.PAPER, vPAPER);
        row.put(RoShamBo5.SCISSORS, vSCISSORS);
        row.put(RoShamBo5.ROCK, vROCK);
    }
    public Outcome compete(RoShamBo5 it) {
        return table.get(this).get(it);
    }
}
  • 使用EnumMap实现类似于表格的方法,从而达到了分发的目的。当然也可以使用二维数组来实现。
Author: SinLapis
Link: http://sinlapis.github.io/2019/07/21/Java编程思想笔记0x12/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.