Java使用枚举实现状态机的方法详解
作者:一只小熊猫呀
Java枚举实现状态机
枚举类型很适合用来实现状态机。状态机可以处于有限数量的特定状态。它们通常根据输入,从一个状态移动到下一个状态,但同时也会存在瞬态。当任务执行完毕后,状态机会立即跳出所有状态。
每个状态都有某些可接受的输入,不同的输入会使状态机从当前状态切换到新的状态。由于枚举限制了可能出现的状态集大小(即状态数量),因此很适合表达(枚举)不同的状态和输入。
每种状态一般也会有某种对应的输出。
自动售货机是个很好的状态机应用的例子。首先,在一个枚举中定义一系列输入:
Input.java
import java.util.Random; public enum Input { NICKEL(5), DIME(10), QUARTER(25), DOLLAR(100), TOOTHPASTE(200), CHIPS(75), SODA(100), SOAP(50), ABORT_TRANSACTION { @Override public int amount() { // Disallow throw new RuntimeException("ABORT.amount()"); } }, STOP { // 这必须是最后一个实例 @Override public int amount() { // 不允许 throw new RuntimeException("SHUT_DOWN.amount()"); } }; int value; // 单位为美分(cents) Input(int value) { this.value = value; } Input() { } int amount() { return value; } ; // In cents static Random rand = new Random(47); public static Input randomSelection() { //不包括 STOP: return values()[rand.nextInt(values().length - 1)]; } }
注意其中两个 Input 有着对应的金额,所以在接口中定义了 amount() 方法。然而,对另外两个 Input 调用 amount() 是不合适的,如果调用就会抛出异常。尽管这是个有点奇怪的机制(在接口中定义一个方法,然后如果在某些具体实现中调用它的话就会抛出异常),但这是枚举的限制所导致的。
VendingMachine(自动售货机)接收到输入后,首先通过 Category(类别) 枚举来对这些输入进行分类,这样就可以在各个类别间切换了。下例演示了枚举是如何使代码变得更清晰、更易于管理的。
VendingMachine.java
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; enum Category { MONEY(Input.NICKEL, Input.DIME, Input.QUARTER, Input.DOLLAR), ITEM_SELECTION(Input.TOOTHPASTE, Input.CHIPS, Input.SODA, 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 int amount = 0; private static Input selection = null; enum StateDuration {TRANSIENT} // 标识 enum enum State { RESTING { @Override void next(Input input) { switch (Category.categorize(input)) { case MONEY: amount += input.amount(); state = ADDING_MONEY; break; case SHUT_DOWN: state = TERMINAL; default: } } }, ADDING_MONEY { @Override void next(Input input) { switch (Category.categorize(input)) { case MONEY: amount += input.amount(); break; case ITEM_SELECTION: selection = input; if (amount < selection.amount()) { System.out.println( "Insufficient money for " + selection); } else { state = DISPENSING; } break; case QUIT_TRANSACTION: state = GIVING_CHANGE; break; case SHUT_DOWN: state = TERMINAL; default: } } }, DISPENSING(StateDuration.TRANSIENT) { @Override void next() { System.out.println("here is your " + selection); amount -= selection.amount(); state = GIVING_CHANGE; } }, GIVING_CHANGE(StateDuration.TRANSIENT) { @Override void next() { if (amount > 0) { System.out.println("Your change: " + amount); amount = 0; } state = RESTING; } }, TERMINAL { @Override void output() { System.out.println("Halted"); } }; private boolean isTransient = false; State() { } State(StateDuration trans) { isTransient = true; } void next(Input input) { throw new RuntimeException("Only call " + "next(Input input) for non-transient states"); } void next() { throw new RuntimeException("Only call next() for " + "StateDuration.TRANSIENT states"); } void output() { System.out.println(amount); } } static void run(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)) .skip(1) // Skip the comment line .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()); } }
下面是用于生成输出的文本文件:
VendingMachine.txt
QUARTER;QUARTER;QUARTER;CHIPS; DOLLAR;DOLLAR;TOOTHPASTE; QUARTER;DIME;ABORT_TRANSACTION; QUARTER;DIME;SODA; QUARTER;DIME;NICKEL;SODA; ABORT_TRANSACTION; STOP;
以下是运行参数配置:
运行结果如下:
因为通过 switch 语句在枚举实例中进行选择操作是最常见的方式(注意,为了使 switch 便于操作枚举,语言层面需要付出额外的代价),所以在组织多个枚举类型时,最常问的问题之一就是“我需要什么东西之上(即以什么粒度)进行 switch”。这里最简单的办法是,回头梳理一遍 VendingMachine,就会发现在每种 State 下,你需要针对输入操作的基本类别进行 switch 操作:投入钱币、选择商品、退出交易、关闭机器。并且在这些类别内,你还可以投入不同类别的货币,选择不同类别的商品。Category 枚举会对不同的 Input 类型进行分类,因此 categorize() 方法可以在 switch 中生成恰当的 Category。这种方法用一个 EnumMap 实现了高效且安全的查询。
如果你研究一下 VendingMachine 类,便会发现每个状态的区别,以及对输入的响应区别。同时还要注意那两个瞬态:在 run() 方法中,售货机等待一个 Input,并且会一直在状态间移动,直到它不再处于某个瞬态中。
VendingMachine 可以通过两种不同的 Supplier 对象,以两种方法来测试。RandomInputSupplier 只需要持续生成除 SHUT_DOWN 以外的任何输入。通过一段较长时间的运行后,就相当于做了一次健康检查,以确定售货机不会偏离到某些无效状态。FileInputSupplier 接收文本形式的输入描述文件,并将它们转换为 enum 实例,然后创建 Input 对象。下面是用于生成以上输出的文本文件:
FileInputSupplier 的构造器将这个文件转换为行级的 Stream 流,并忽略注释行。然后它通过 String.split() 方法将每一行都根据分号拆开。这样就能生成一个字符串数组,可以通过先将该数组转化为 Stream,然后执行 flatMap(),来将其注入(前面 FileInputSupplier 中生成的)Stream 中。结果将删除所有的空格,并转换为 List,并从中得到 Iterator。
上述设计有个限制:VendingMachine 中会被 State 枚举实例访问到的字段都必须是静态的,这意味着只能存在一个 VendingMachine 实例。这可能不会是个大问题——你可以想想一个实际的(嵌入式Java)实现,每台机器可能就只有一个应用程序。
到此这篇关于Java使用枚举实现状态机的方法详解的文章就介绍到这了,更多相关Java枚举实现状态机内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!