手机网站开发利用流程站长友情链接平台
目录
常量特定方法
职责链模式的枚举实现
状态机模式的枚举实现
多路分发
1、使用枚举类型实现分发
2、使用常量特定方法实现分发
3、使用EnumMap实现分发
4、使用二维数组实现分发
本笔记参考自: 《On Java 中文版》
常量特定方法
在Java中,我们可以通过为每个枚举实例编写不同的方法,来赋予它们不同的行为。一个方式是在枚举类型中定义抽象方法,并在枚举实例中实现它:
【例子:为枚举实例编写方法】
import java.text.DateFormat;
import java.util.Date;public enum ConstantSpecificMethod {DATE_TIME {@OverrideString getInfo() {return DateFormat.getDateInstance().format(new Date());}},CLASSPATH {@OverrideString getInfo() {// getenv()方法用于获取指定环境变量的值return System.getenv("CLASSPATH");}},VERSION {@OverrideString getInfo() { // 获取由输入指定的属性return System.getProperty("java.version");}};abstract String getInfo();public static void main(String[] args) {for (ConstantSpecificMethod csm : values())System.out.println(csm.getInfo());}
}
程序执行的结果是:
在这个例子中,我们通过关联的枚举实例来调用对应的方法,这通常被称为表驱动模式。
这里需要复习一点:在面向对象的编程中,不同的行为是和不同的类相关联的。而上述枚举的每个实例都拥有各自不同的行为,这就表明每个实例都相当于不同的类型(体现了多态)。它们的共同点在于它们的基类ConstantSpecificMethod。
但这并不意味着枚举实例能够等价于普通的类:
【例子:枚举实例和类之间的不同点】
enum LikeClasses {A {@Overridevoid behavior() {System.out.println("A");}},B {@Overridevoid behavior() {System.out.println("B");}},C {@Overridevoid behavior() {System.out.println("C");}};abstract void behavior();
}public class NotClasses {
// void f1(LikeClasses.A instance) {
// } // 不可行的操作
}
编译器并不允许f1()的操作:将枚举实例作为类的类型进行使用。为了解释这一点,可以对程序进行反编译(javap -c LikeClasses):
反编译告诉我们,每个枚举元素都是LikeClasses的一个static final实例。实例无法作为类型进行使用。
除此之外,不同于内部类,因为枚举实例是静态的,所以我们无法通过外部类LikeClasses直接访问其内部的枚举实例(具体而言,我们无法在非静态域中访问外部类的静态成员)。
(与匿名内部类相比,常量特定方法要显得更加简洁。)
除此之外,常量特定方法也支持重写:
【例子:重写常量特定方法】
public enum OverrideConstantSpecific {NUT, BOLT,WASHER {@Overridepublic void f() {System.out.println("重写后的f()方法");}};void f() {System.out.println("默认的f()方法");}public static void main(String[] args) {for (OverrideConstantSpecific ocs : values()) {System.out.print(ocs + ": ");ocs.f();}}
}
程序执行的结果是:
这些特性使得我们在通常情况下,可以将枚举作为类来使用。
职责链模式的枚举实现
||| 职责链模式:先创建一批用于解决目标问题的不同方法,并将它们链式排列。当一个请求到达时,它会顺着这条“链”向下走,直到遇到可以解决当前请求的方法。
下面的例子描述了一个邮局模型:使用常规方法处理信件,当前方法不可行时,尝试其他方法,直到无法处理该信件为止(称为“死信”)。
可以将每种处理方法视作一种策略,策略的列表组成了一条职责链。
【例子:邮局模型】
import onjava.Enums;import java.util.Iterator;class Mail {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++;@Overridepublic String toString() {return "邮件" + id;}public String details() {return toString() +",一般交付:" + generalDelivery + ",地址是否可以扫描:" + scannability +"\n地址是否可读:" + readability + ",目标地址:" + address +"\n返还地址:" + returnAddress;}// 用于生成测试邮件:// (Enums类可见笔记 进阶1-1)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;}// count无需修改,可以使用finalpublic static Iterable<Mail> generator(final int count) {return new Iterable<Mail>() {int n = count;@Overridepublic Iterator<Mail> iterator() {return new Iterator<Mail>() {@Overridepublic boolean hasNext() {return n-- > 0;}@Overridepublic Mail next() {return randomMail();}};}};}
}public class PostOffice {// 使用职责链模式:enum MailHandler {GENERAL_DELIVERY {@Overrideboolean handle(Mail m) {switch (m.generalDelivery) {case YES:System.out.println("对" + m + "使用常规方法进行处理");return true;default:return false;}}},MACHINE_SCAN {@Overrideboolean handle(Mail m) {switch (m.scannability) {case UNSCANNABLE:return false;default:switch (m.address) {case INCORRECT:return false;default:System.out.println("自动派送" + m);return true;}}}},VISUAL_INSPECTION {@Overrideboolean handle(Mail m) {switch (m.readability) {case ILLEGIBLE:return false;default:switch (m.address) {case INCORRECT:return false;default:System.out.println("常规派送" + m);return true;}}}},RETURN_TO_SENDER {@Overrideboolean handle(Mail m) {switch (m.returnAddress) {case MISSING:return false;default:System.out.println("将" + m + "返还给发送者");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 + "是一封死信");}public static void main(String[] args) {for (Mail mail : Mail.generator(5)) {System.out.println(mail.details());handle(mail);System.out.println("============");}}
}
程序执行的结果是:
职责链模式在MailHandler枚举中得到应用,并且枚举的定义顺序决定了各个策略在邮件上的应用顺序。
状态机模式的枚举实现
||| 状态机的解释:
- 具有有限数量的特定状态。
- 每个状态都有输入。根据输入,可以实现状态之间的跳转,但也存在瞬态。
- 当任务执行完毕后,立即跳出所有状态。
用枚举表示状态机模式的一个优势是,枚举可以限制出现的状态集的大小:
【例子:自动售货机】
首先创建一个用于输入的枚举:
import java.util.Random;public enum Input {CENT(1), COIN(10), TEN_COIN(100),TOOTHBRUSH(150.0f), TOWEL(200.0f), SOAP(80.0f),ABORT_TRANSACTION {@Overridepublic float amount() {throw new RuntimeException("ABORT.amount()");}},STOP { // STOP必须是最后一个实例@Overridepublic float amount() {throw new RuntimeException("SHUT_DOWN.amount()");}};float value;Input(float value) {this.value = value;}Input() {}float amount() { // 单位为:毛return value;}static Random rand = new Random(47);public static Input randomSelection() {// 不包括STOP:return values()[rand.nextInt(values().length - 1)];}
}
amount()会返回物品对应的价格。由于枚举的限制,amount()会作用于所有的枚举实例。很显然,对最后两个实例调用它并不合适,因此这里需要重写amount()。
接下来实现自动售货机的状态机:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.function.Supplier;
import java.util.stream.Collectors;enum Category {MONEY(Input.CENT, Input.COIN, Input.TEN_COIN),ITEM_SELECTION(Input.TOOTHBRUSH, Input.TOWEL, Input.SOAP),QUIT_TRANSACTION(Input.ABORT_TRANSACTION),SHUT_DOWN(Input.STOP);private Input[] values;Category(Input... types) {values = types;}private static EnumMap<Input, Category> categories =new EnumMap<>(Input.class);static {for (Category c : Category.class.getEnumConstants())for (Input type : c.values)categories.put(type, c);}public static Category categorize(Input input) {return categories.get(input);}
}public class VendingMachine {private static State state = State.RESTING;private static float amount = 0;private static Input selection = null;enum StateDuration { // 用于标记enum中的瞬态TRANSIENT;}enum State {RESTING { // 休眠中@Overridevoid next(Input input) {switch (Category.categorize(input)) {case MONEY:amount += input.amount();state = ADDING_MONEY; // 重新赋值statebreak;case SHUT_DOWN:state = TERMINAL;default:}}},ADDING_MONEY { // 添加金钱,购买商品@Overridevoid 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("当前余额不足以购买" + selection);elsestate = DISPENSING; // 下个状态也可以是瞬态break;case QUIT_TRANSACTION:state = GIVING_CHANGE;break;case SHUT_DOWN:state = TERMINAL;default:}}},DISPENSING(StateDuration.TRANSIENT) { // 取出商品(注意:实例的初始化方式并不相同)@Overridevoid next() {System.out.println("这是你购买的物品:" + selection);amount -= selection.amount();state = GIVING_CHANGE;}},GIVING_CHANGE(StateDuration.TRANSIENT) { // 找钱@Overridevoid next() {if (amount > 0) {System.out.println("这是找零:" + amount);amount = 0;}state = RESTING;}},TERMINAL { // 关闭机器@Overridevoid output() {System.out.println("停止运行");}};// 判断当前状态是否为瞬态private boolean isTransient = false;State() {}State(StateDuration trans) {isTransient = true; // 说明当前处于瞬态}void next(Input input) {throw new RuntimeException("仅应该对非瞬态调用next(Input input)");}void next() {throw new RuntimeException("仅应该对StateDuration.TRANSIENT(瞬态)调用next()");}void output() {System.out.println(amount);}}static void run(Supplier<Input> gen) {while (state != State.TERMINAL) {state.next(gen.get());while (state.isTransient)state.next();state.output();}}public static void main(String[] args) {Supplier<Input> gen = new RandomInputSupplier();if (args.length == 1)gen = new FileInputSupplier(args[0]);run(gen);}
}class RandomInputSupplier implements Supplier<Input> {@Overridepublic Input get() {return Input.randomSelection();}
}// 从文件输入:
class FileInputSupplier implements Supplier<Input> {private Iterator<String> input;FileInputSupplier(String fileName) {try {input = Files.lines(Paths.get(fileName)) // 行级的Stream流.flatMap(s -> Arrays.stream(s.split(";"))) // 输入文件的信息是由分号分隔的.map(String::trim).collect(Collectors.toList()).iterator();} catch (IOException e) {throw new RuntimeException(e);}}@Overridepublic Input get() {if (!input.hasNext())return null;return Enum.valueOf(Input.class, input.next().trim());}
}
若不从文件导入数据,则售货机在开机后就会一直运行下去。为了完整演示整个状态机的流程,这里使用文件输入。测试文件如下:
CENT; TEN_COIN; TOOTHBRUSH;
COIN; TEN_COIN; TEN_COIN; SOAP;
TEN_COIN; TEN_COIN; CENT; COIN; TOWEL;
ABORT_TRANSACTION;
STOP;
对应该文件,程序执行的结果是:
在这种模式中,枚举在switch上发挥了它的功能。
在VendingMachine类中存在着两个瞬态:DISPENSING和GIVING_CHANGE。状态机会在各种状态之间移动,知道不再处于某个瞬态中为止(对这个例子而言,“取出商品”和“找零”的动作是瞬态,售货机不应该停留在这些状态中)。
然而,这种设计存在一个限制:State的枚举实例是静态的,这意味着这些实例只能访问其外部类(即VendingMachine)中的静态字段。这意味着只能存在一个VendingMachine实例。
多路分发
若一个程序中存在多个交互类型,这个程序有可能变得很混乱。举一个例子:
public abstract class Number {abstract Number plus(Number number);abstract Number multiply(Number number);//...
}
此处的Number类是某个系统中所有数值类型的基类,其中包括一些进行数值运算所需要的方法。
现在假设a和b分别属于Number的某个子类,并且都使用Number引用指示它们。若要处理语句a.plus(b),一个简单的想法是我们需要同时知道a、b的具体类型。遗憾的是,Java只能进行单路分发,这意味着我们只能对一个类型未知的对象进行操作。
多路分发并不是同时对多个类型未知的对象进行操作,而是进行了多次的方法调用。
使用多路分发,就必须对每个类型都执行虚拟调用。并且,若操作发生在不同的交互类型层次结构中,则还需要对每个层次结构都执行虚拟操作。
接下来通过一个“猜拳”的例子展示多路分发的使用:
【例子:“猜拳”游戏】
首先确定“猜拳”的三种结果:
public enum Outcome {WIN, // 赢LOSE, // 输DRAW // 平局
}
接下来实现多路分发的接口:
public interface Item {Outcome compete(Item it);Outcome eval(Paper p);Outcome eval(Scissors p);Outcome eval(Rock p);
}
接下来为“石头”、“剪刀”和“布”分别匹配它们的胜负情况:
然后,通过调用compete()方法,就可以简单地判断“猜拳”的胜负:
import java.util.Random;public class RoShamBo1 {static final int SIZE = 10;private static Random rand = new Random(47);public static Item newItem() {switch (rand.nextInt(3)) {default: // 必须有的defaultcase 0:return new Scissors();case 1:return new Paper();case 2:return new Rock();}}public static void match(Item a, Item b) {System.out.println(a + " vs. " + b + " " + a.compete(b));}public static void main(String[] args) {for (int i = 0; i < SIZE; i++)match(newItem(), newItem());}
}
程序执行的结果是:
在match()方法中,程序可以知道a的类型,进而调用对应的compete()方法。在compete()中调用eval()方法时,会向其中传入一个this引用,这个this引用用于保留原本a的类型。
如上所示,多路分发的构筑较为麻烦。但好处显而易见:我们在调用中保持住了语法的优雅。
因为枚举实例不是类型,因此我们无法通过重载eval()方法的方式实现目标。但还是有别的方法可以进行实现。
1、使用枚举类型实现分发
一种方法是在初始化枚举实例时进行操作。通过这种方式,我们最终可以得到一个类似于查询表的结构:
【例子:初始化枚举实现“猜拳”】
import static enums.Outcome.*;// 规定Competitor接口,通过接口来操作枚举:
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 paper,Outcome scissors, Outcome rock) {this.vPAPER = paper;this.vSCISSORS = scissors;this.vROCK = rock;}@Overridepublic Outcome compete(RoShamBo2 competitor) {switch (competitor) {default:case ROCK: // 若对方出的是石头return vROCK; // 那么就返回当前对象遇到石头时会得出的结果case PAPER: // 布return vPAPER;case SCISSORS: // 剪刀return vSCISSORS;}}public static void main(String[] args) {RoShamBo.play(RoShamBo2.class, 10); // 独立出来的方法,用于操作猜拳}
}
程序执行的结果是:
书本在此处将原本用于操作枚举的代码分隔开来,以此增加了代码的复用率。以下是复用的代码(Competitor接口及RoShamBo类)的实现:
【例子:用于操作枚举的工具】
为了统一现有的枚举及之后会出现的枚举的操作,可以使用Competitor接口:
public interface Competitor<T extends Competitor<T>> {Outcome compete(T competitor);
}
我们规定,所有的“石头剪刀布”枚举都来自于这种竞争者(Competitor)关系,自限定类型(笔记18-4)可以很好地满足这一点。
与一般的泛型相比,自限定类型对继承关系有更加严格的要求。对于上述的compete()方法而言,它只会接受Competitor及其子类。
然后是生成随机结果的RoShamBo类:
import onjava.Enums;public class RoShamBo extends EnumClass {// 使用了自限定类型: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));}
}
这个类包含了一个用于接收的paly()方法和真正调用枚举操作的match()方法。play()方法规定,传入的数据必须是一个实现了Competitor接口的枚举。
需要注意一点:由于play()方法不需要返回值,换言之,在离开泛型的边界上并没有需要使用到类型参数T的地方,因此参数T在这里似乎是不必要的。但如果将Class<T>替换为Class<?>这种通配符形式,就会发生报错:
这是因为通配符【?】无法继承多个基类,因此这里只能使用类型参数T。
2、使用常量特定方法实现分发
遗憾的是,枚举本身并不能作为对象类型使用。因此我们没有办法通过传递实例来进行多路分发。在这里,一个好的办法是使用switch语句:
【例子:使用switch分发】
import static enums.Outcome.*;public enum RoShamBo3 implements Competitor<RoShamBo3> {PAPER {@Overridepublic Outcome compete(RoShamBo3 competitor) {switch (competitor) {default:case PAPER:return DRAW;case SCISSORS:return LOSE;case ROCK:return WIN;}}},SCISSORS {@Overridepublic Outcome compete(RoShamBo3 competitor) {switch (competitor) {default:case PAPER:return WIN;case SCISSORS:return DRAW;case ROCK:return LOSE;}}},ROCK {@Overridepublic Outcome compete(RoShamBo3 competitor) {switch (competitor) {default:case PAPER:return LOSE;case SCISSORS:return WIN;case ROCK:return DRAW;}}};// 此处的方法声明只是为了便于理解:@Overridepublic abstract Outcome compete(RoShamBo3 competitor);public static void main(String[] args) {RoShamBo.play(RoShamBo3.class, 10);}
}
程序执行的结果与上一个例子相同:
这种写法无疑是繁琐的。因此,我们可以尝试对其进行优化。用三目运算符来取代switch语句,压缩代码空间:
【例子:使用三目运算符进行优化】
public enum RoShamBo4 implements Competitor<RoShamBo4> {ROCK {@Overridepublic Outcome compete(RoShamBo4 competitor) {return compete(SCISSORS, competitor);}},SCISSORS {@Overridepublic Outcome compete(RoShamBo4 competitor) {return compete(PAPER, competitor);}},PAPER {@Overridepublic Outcome compete(RoShamBo4 competitor) {return compete(ROCK, competitor);}};// 重载的compete()方法// 第一个参数表示的是胜利条件Outcome compete(RoShamBo4 loser, RoShamBo4 competitor) {return ((competitor == this) ? Outcome.DRAW : // 通过this确定平局条件(competitor == loser) ? Outcome.WIN : // 通过传入参数确定胜利条件Outcome.LOSE); // 那么剩下的就是失败条件了}public static void main(String[] args) {RoShamBo.play(RoShamBo4.class, 10);}
}
由于输出结果是一致的,因此此处不再展示。
当然,更加简短的代码有可能带来更大的理解成本。在大型的系统开发过程中应该要注意这一点。
3、使用EnumMap实现分发
EnumMap是专门为enum设计的高效Map。使用它,我们可以更好地实现多路分发。具体而言,当我们需要进行两路分发时,我们可以使用嵌套的EnumMap:
【例子:使用EnumMap实现分发】
import java.util.EnumMap;import static enums.Outcome.*;public enum RoShamBo5 implements Competitor<RoShamBo5> {PAPER, SCISSORS, ROCK;// 使用Class进行初始化,规定Map的键值必须是RoShamBo5类型static EnumMap<RoShamBo5, EnumMap<RoShamBo5, Outcome>>table = new EnumMap<>(RoShamBo5.class);static {// 使用for循环,为table每个键创建其对应的对象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); // get()方法会返回指定键对应的值(一个EnumMap的对象)row.put(RoShamBo5.PAPER, vPAPER);row.put(RoShamBo5.SCISSORS, vSCISSORS);row.put(RoShamBo5.ROCK, vROCK);}@Overridepublic Outcome compete(RoShamBo5 competitor) {return table.get(this).get(competitor); // 两次get(),两次分发}public static void main(String[] args) {RoShamBo.play(RoShamBo5.class, 10);}
}
这里需要注意的是static子句。我们需要在RoShamBo5的其余部分初始化完毕之前,将所有的胜负信息填充到table里面。因此需要使用静态的初始化方式。
4、使用二维数组实现分发
在“石头剪刀布”中,每一个枚举实例都会对应一个固定的值(可以用ordinal()获取)。这使得使用二维数组实现两路分发成为可能:
【例子:使用二维数组实现分发】
import static enums.Outcome.*;public enum RoShamBo6 implements Competitor<RoShamBo6> {PAPER, SCISSORS, ROCK;// 胜负顺序需要根据枚举的定义顺序进行调整:private static Outcome[][] table = {{DRAW, LOSE, WIN}, // 布{WIN, DRAW, LOSE}, // 剪刀{LOSE, WIN, DRAW}, // 石头};@Overridepublic Outcome compete(RoShamBo6 competitor) {return table[this.ordinal()][this.ordinal()];}public static void main(String[] args) {RoShamBo.play(RoShamBo6.class, 10);}
}
这种写法有着极高的效率,以及简短易懂的代码。但与之相对,它的缺点也正是在于其使用了数组:一方面,在面对更加大量的数据时,使用数组或许会漏掉某些麻烦的情况;除此之外,这种写法更为死板——从数组中得到的依旧只是一个常量的结果。
针对第二个缺点,我们可以使用函数对象来进行一些变化,使得数组不再那么僵硬。并且,在处理特定问题时,“表驱动模式”可以发挥出强大的力量。