目录
本笔记参考自: 《On Java 中文版》
??????? 在Java中,我们可以通过为每个枚举实例编写不同的方法,来赋予它们不同的行为。一个方式是在枚举类型中定义抽象方法,并在枚举实例中实现它:
【例子:为枚举实例编写方法】
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() {// getenv()方法用于获取指定环境变量的值
return System.getenv("CLASSPATH");
}
},
VERSION {
@Override
String getInfo() { // 获取由输入指定的属性
return System.getProperty("java.version");
}
};
abstract String getInfo();
public static void main(String[] args) {
for (ConstantSpecificMethod csm : values())
System.out.println(csm.getInfo());
}
}
??????? 程序执行的结果是:
??????? 在这个例子中,我们通过关联的枚举实例来调用对应的方法,这通常被称为表驱动模式。
??????? 这里需要复习一点:在面向对象的编程中,不同的行为是和不同的类相关联的。而上述枚举的每个实例都拥有各自不同的行为,这就表明每个实例都相当于不同的类型(体现了多态)。它们的共同点在于它们的基类ConstantSpecificMethod。
??????? 但这并不意味着枚举实例能够等价于普通的类:
【例子:枚举实例和类之间的不同点】
enum LikeClasses {
A {
@Override
void behavior() {
System.out.println("A");
}
},
B {
@Override
void behavior() {
System.out.println("B");
}
},
C {
@Override
void 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 {
@Override
public 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++;
@Override
public 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无需修改,可以使用final
public static Iterable<Mail> generator(final int count) {
return new Iterable<Mail>() {
int n = count;
@Override
public Iterator<Mail> iterator() {
return new Iterator<Mail>() {
@Override
public boolean hasNext() {
return n-- > 0;
}
@Override
public Mail next() {
return randomMail();
}
};
}
};
}
}
public class PostOffice {
// 使用职责链模式:
enum MailHandler {
GENERAL_DELIVERY {
@Override
boolean handle(Mail m) {
switch (m.generalDelivery) {
case YES:
System.out.println(
"对" + 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("自动派送" + m);
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("常规派送" + m);
return true;
}
}
}
},
RETURN_TO_SENDER {
@Override
boolean 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 {
@Override
public float amount() {
throw new RuntimeException("ABORT.amount()");
}
},
STOP { // STOP必须是最后一个实例
@Override
public 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 { // 休眠中
@Override
void next(Input input) {
switch (Category.categorize(input)) {
case MONEY:
amount += input.amount();
state = ADDING_MONEY; // 重新赋值state
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("当前余额不足以购买" + 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("这是你购买的物品:" + selection);
amount -= selection.amount();
state = GIVING_CHANGE;
}
},
GIVING_CHANGE(StateDuration.TRANSIENT) { // 找钱
@Override
void next() {
if (amount > 0) {
System.out.println("这是找零:" + amount);
amount = 0;
}
state = RESTING;
}
},
TERMINAL { // 关闭机器
@Override
void 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> {
@Override
public 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);
}
}
@Override
public 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: // 必须有的default
case 0:
return new Scissors();
case 1:
return new Paper();
case 2:
return new Rock();
}
}
public static void match(Item a, Item b) {
System.out.println(a + " vs. " + b + " " + a.compete(b));
}
public static void main(String[] args) {
for (int i = 0; i < SIZE; i++)
match(newItem(), newItem());
}
}
??????? 程序执行的结果是:
??????? 在match()方法中,程序可以知道a的类型,进而调用对应的compete()方法。在compete()中调用eval()方法时,会向其中传入一个this引用,这个this引用用于保留原本a的类型。
??? 如上所示,多路分发的构筑较为麻烦。但好处显而易见:我们在调用中保持住了语法的优雅。
??????? 因为枚举实例不是类型,因此我们无法通过重载eval()方法的方式实现目标。但还是有别的方法可以进行实现。?
??????? 一种方法是在初始化枚举实例时进行操作。通过这种方式,我们最终可以得到一个类似于查询表的结构:
【例子:初始化枚举实现“猜拳”】
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;
}
@Override
public 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。
??????? 遗憾的是,枚举本身并不能作为对象类型使用。因此我们没有办法通过传递实例来进行多路分发。在这里,一个好的办法是使用switch语句:
【例子:使用switch分发】
import static enums.Outcome.*;
public enum RoShamBo3 implements Competitor<RoShamBo3> {
PAPER {
@Override
public Outcome compete(RoShamBo3 competitor) {
switch (competitor) {
default:
case PAPER:
return DRAW;
case SCISSORS:
return LOSE;
case ROCK:
return WIN;
}
}
},
SCISSORS {
@Override
public Outcome compete(RoShamBo3 competitor) {
switch (competitor) {
default:
case PAPER:
return WIN;
case SCISSORS:
return DRAW;
case ROCK:
return LOSE;
}
}
},
ROCK {
@Override
public Outcome compete(RoShamBo3 competitor) {
switch (competitor) {
default:
case PAPER:
return LOSE;
case SCISSORS:
return WIN;
case ROCK:
return DRAW;
}
}
};
// 此处的方法声明只是为了便于理解:
@Override
public abstract Outcome compete(RoShamBo3 competitor);
public static void main(String[] args) {
RoShamBo.play(RoShamBo3.class, 10);
}
}
??????? 程序执行的结果与上一个例子相同:
??????? 这种写法无疑是繁琐的。因此,我们可以尝试对其进行优化。用三目运算符来取代switch语句,压缩代码空间:
【例子:使用三目运算符进行优化】
public enum RoShamBo4 implements Competitor<RoShamBo4> {
ROCK {
@Override
public Outcome compete(RoShamBo4 competitor) {
return compete(SCISSORS, competitor);
}
},
SCISSORS {
@Override
public Outcome compete(RoShamBo4 competitor) {
return compete(PAPER, competitor);
}
},
PAPER {
@Override
public 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);
}
}
??????? 由于输出结果是一致的,因此此处不再展示。
??? 当然,更加简短的代码有可能带来更大的理解成本。在大型的系统开发过程中应该要注意这一点。
??????? 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);
}
@Override
public 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里面。因此需要使用静态的初始化方式。
??????? 在“石头剪刀布”中,每一个枚举实例都会对应一个固定的值(可以用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}, // 石头
};
@Override
public Outcome compete(RoShamBo6 competitor) {
return table[this.ordinal()][this.ordinal()];
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo6.class, 10);
}
}
??????? 这种写法有着极高的效率,以及简短易懂的代码。但与之相对,它的缺点也正是在于其使用了数组:一方面,在面对更加大量的数据时,使用数组或许会漏掉某些麻烦的情况;除此之外,这种写法更为死板——从数组中得到的依旧只是一个常量的结果。
??? 针对第二个缺点,我们可以使用函数对象来进行一些变化,使得数组不再那么僵硬。并且,在处理特定问题时,“表驱动模式”可以发挥出强大的力量。