Java使用枚举实现状态机的方法详解

 更新时间:2023年11月09日 10:30:25   作者:一只小熊猫呀  
这篇文章主要介绍了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枚举实现状态机内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 实体类或对象序列化时,忽略为空属性的操作

    实体类或对象序列化时,忽略为空属性的操作

    这篇文章主要介绍了实体类或对象序列化时,忽略为空属性的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Java详细分析连接数据库的流程

    Java详细分析连接数据库的流程

    Java数据库连接,JDBC是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。JDBC也是Sun Microsystems的商标。我们通常说的JDBC是面向关系型数据库的
    2022-05-05
  • Spring MVC 中 短信验证码功能的实现方法

    Spring MVC 中 短信验证码功能的实现方法

    短信验证功能在各个网站应用都非常广泛,那么在springmvc中如何实现短信验证码功能呢?今天小编抽时间给大家介绍下Spring MVC 中 短信验证码功能的实现方法,一起看看吧
    2016-09-09
  • Java实现Consul/Nacos根据GPU型号、显存余量执行负载均衡的步骤详解

    Java实现Consul/Nacos根据GPU型号、显存余量执行负载均衡的步骤详解

    这篇文章主要介绍了Java实现Consul/Nacos根据GPU型号、显存余量执行负载均衡的步骤详解,本文分步骤结合实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2025-04-04
  • 初识JAVA数组

    初识JAVA数组

    java语言中,数组是一种最简单的复合数据类型。数组是有序数据的集合,数组中的每个元素具有相同的数据类型,可以用一个统一的数组名和下标来唯一地确定数组中的元素。数组有一维数组和多维数组。
    2014-08-08
  • Java中的方法、常量、变量、参数用例详解

    Java中的方法、常量、变量、参数用例详解

    在JVM的运转中,承载的是数据,而数据的一种变现形式就是“量”,量分为:常量与变量,我们在数学和物理学中已经接触过变量的概念了,在Java中的变量就是在程序运行过程中可以改变其值的量,这篇文章主要介绍了Java中的方法、常量、变量、参数,需要的朋友可以参考下
    2024-01-01
  • Java获取路径的6种方式代码示例

    Java获取路径的6种方式代码示例

    在Java中获取路径的方法有多种,每种方法适用于不同的场景,这篇文章主要介绍了Java获取路径的6种方式,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-02-02
  • java8新特性教程之time包使用总结

    java8新特性教程之time包使用总结

    Java8新增了date和time的util包,下面这篇文章主要给大家介绍了关于java8新特性教程之time包使用的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-01-01
  • Java进行反编译生成.java文件方式(javap、jad下载安装使用)

    Java进行反编译生成.java文件方式(javap、jad下载安装使用)

    这篇文章主要介绍了Java进行反编译生成.java文件方式(javap、jad下载安装使用),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • 使用dom4j递归解析节点内还含有多个节点的xml

    使用dom4j递归解析节点内还含有多个节点的xml

    这篇文章主要介绍了使用dom4j递归解析节点内还含有多个节点的xml,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09

最新评论