Java使用枚舉實(shí)現(xiàn)狀態(tài)機(jī)的方法詳解
Java枚舉實(shí)現(xiàn)狀態(tài)機(jī)
枚舉類型很適合用來(lái)實(shí)現(xiàn)狀態(tài)機(jī)。狀態(tài)機(jī)可以處于有限數(shù)量的特定狀態(tài)。它們通常根據(jù)輸入,從一個(gè)狀態(tài)移動(dòng)到下一個(gè)狀態(tài),但同時(shí)也會(huì)存在瞬態(tài)。當(dāng)任務(wù)執(zhí)行完畢后,狀態(tài)機(jī)會(huì)立即跳出所有狀態(tài)。
每個(gè)狀態(tài)都有某些可接受的輸入,不同的輸入會(huì)使?fàn)顟B(tài)機(jī)從當(dāng)前狀態(tài)切換到新的狀態(tài)。由于枚舉限制了可能出現(xiàn)的狀態(tài)集大小(即狀態(tài)數(shù)量),因此很適合表達(dá)(枚舉)不同的狀態(tài)和輸入。
每種狀態(tài)一般也會(huì)有某種對(duì)應(yīng)的輸出。
自動(dòng)售貨機(jī)是個(gè)很好的狀態(tài)機(jī)應(yīng)用的例子。首先,在一個(gè)枚舉中定義一系列輸入:
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 { // 這必須是最后一個(gè)實(shí)例 @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)]; } }
注意其中兩個(gè) Input 有著對(duì)應(yīng)的金額,所以在接口中定義了 amount() 方法。然而,對(duì)另外兩個(gè) Input 調(diào)用 amount() 是不合適的,如果調(diào)用就會(huì)拋出異常。盡管這是個(gè)有點(diǎn)奇怪的機(jī)制(在接口中定義一個(gè)方法,然后如果在某些具體實(shí)現(xiàn)中調(diào)用它的話就會(huì)拋出異常),但這是枚舉的限制所導(dǎo)致的。
VendingMachine(自動(dòng)售貨機(jī))接收到輸入后,首先通過(guò) Category(類別) 枚舉來(lái)對(duì)這些輸入進(jìn)行分類,這樣就可以在各個(gè)類別間切換了。下例演示了枚舉是如何使代碼變得更清晰、更易于管理的。
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} // 標(biāo)識(shí) 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); } } // 基本的穩(wěn)健性檢查: class RandomInputSupplier implements Supplier<Input> { @Override public Input get() { return Input.randomSelection(); } } // 從以“;”分割的字符串的文件創(chuàng)建輸入 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;
以下是運(yùn)行參數(shù)配置:
運(yùn)行結(jié)果如下:
因?yàn)橥ㄟ^(guò) switch 語(yǔ)句在枚舉實(shí)例中進(jìn)行選擇操作是最常見的方式(注意,為了使 switch 便于操作枚舉,語(yǔ)言層面需要付出額外的代價(jià)),所以在組織多個(gè)枚舉類型時(shí),最常問(wèn)的問(wèn)題之一就是“我需要什么東西之上(即以什么粒度)進(jìn)行 switch”。這里最簡(jiǎn)單的辦法是,回頭梳理一遍 VendingMachine,就會(huì)發(fā)現(xiàn)在每種 State 下,你需要針對(duì)輸入操作的基本類別進(jìn)行 switch 操作:投入錢幣、選擇商品、退出交易、關(guān)閉機(jī)器。并且在這些類別內(nèi),你還可以投入不同類別的貨幣,選擇不同類別的商品。Category 枚舉會(huì)對(duì)不同的 Input 類型進(jìn)行分類,因此 categorize() 方法可以在 switch 中生成恰當(dāng)?shù)?Category。這種方法用一個(gè) EnumMap 實(shí)現(xiàn)了高效且安全的查詢。
如果你研究一下 VendingMachine 類,便會(huì)發(fā)現(xiàn)每個(gè)狀態(tài)的區(qū)別,以及對(duì)輸入的響應(yīng)區(qū)別。同時(shí)還要注意那兩個(gè)瞬態(tài):在 run() 方法中,售貨機(jī)等待一個(gè) Input,并且會(huì)一直在狀態(tài)間移動(dòng),直到它不再處于某個(gè)瞬態(tài)中。
VendingMachine 可以通過(guò)兩種不同的 Supplier 對(duì)象,以兩種方法來(lái)測(cè)試。RandomInputSupplier 只需要持續(xù)生成除 SHUT_DOWN 以外的任何輸入。通過(guò)一段較長(zhǎng)時(shí)間的運(yùn)行后,就相當(dāng)于做了一次健康檢查,以確定售貨機(jī)不會(huì)偏離到某些無(wú)效狀態(tài)。FileInputSupplier 接收文本形式的輸入描述文件,并將它們轉(zhuǎn)換為 enum 實(shí)例,然后創(chuàng)建 Input 對(duì)象。下面是用于生成以上輸出的文本文件:
FileInputSupplier 的構(gòu)造器將這個(gè)文件轉(zhuǎn)換為行級(jí)的 Stream 流,并忽略注釋行。然后它通過(guò) String.split() 方法將每一行都根據(jù)分號(hào)拆開。這樣就能生成一個(gè)字符串?dāng)?shù)組,可以通過(guò)先將該數(shù)組轉(zhuǎn)化為 Stream,然后執(zhí)行 flatMap(),來(lái)將其注入(前面 FileInputSupplier 中生成的)Stream 中。結(jié)果將刪除所有的空格,并轉(zhuǎn)換為 List,并從中得到 Iterator。
上述設(shè)計(jì)有個(gè)限制:VendingMachine 中會(huì)被 State 枚舉實(shí)例訪問(wèn)到的字段都必須是靜態(tài)的,這意味著只能存在一個(gè) VendingMachine 實(shí)例。這可能不會(huì)是個(gè)大問(wèn)題——你可以想想一個(gè)實(shí)際的(嵌入式Java)實(shí)現(xiàn),每臺(tái)機(jī)器可能就只有一個(gè)應(yīng)用程序。
到此這篇關(guān)于Java使用枚舉實(shí)現(xiàn)狀態(tài)機(jī)的方法詳解的文章就介紹到這了,更多相關(guān)Java枚舉實(shí)現(xiàn)狀態(tài)機(jī)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
實(shí)體類或?qū)ο笮蛄谢瘯r(shí),忽略為空屬性的操作
這篇文章主要介紹了實(shí)體類或?qū)ο笮蛄谢瘯r(shí),忽略為空屬性的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Java詳細(xì)分析連接數(shù)據(jù)庫(kù)的流程
Java數(shù)據(jù)庫(kù)連接,JDBC是Java語(yǔ)言中用來(lái)規(guī)范客戶端程序如何來(lái)訪問(wèn)數(shù)據(jù)庫(kù)的應(yīng)用程序接口,提供了諸如查詢和更新數(shù)據(jù)庫(kù)中數(shù)據(jù)的方法。JDBC也是Sun Microsystems的商標(biāo)。我們通常說(shuō)的JDBC是面向關(guān)系型數(shù)據(jù)庫(kù)的2022-05-05Spring MVC 中 短信驗(yàn)證碼功能的實(shí)現(xiàn)方法
短信驗(yàn)證功能在各個(gè)網(wǎng)站應(yīng)用都非常廣泛,那么在springmvc中如何實(shí)現(xiàn)短信驗(yàn)證碼功能呢?今天小編抽時(shí)間給大家介紹下Spring MVC 中 短信驗(yàn)證碼功能的實(shí)現(xiàn)方法,一起看看吧2016-09-09Java實(shí)現(xiàn)Consul/Nacos根據(jù)GPU型號(hào)、顯存余量執(zhí)行負(fù)載均衡的步驟詳解
這篇文章主要介紹了Java實(shí)現(xiàn)Consul/Nacos根據(jù)GPU型號(hào)、顯存余量執(zhí)行負(fù)載均衡的步驟詳解,本文分步驟結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2025-04-04Java進(jìn)行反編譯生成.java文件方式(javap、jad下載安裝使用)
這篇文章主要介紹了Java進(jìn)行反編譯生成.java文件方式(javap、jad下載安裝使用),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12使用dom4j遞歸解析節(jié)點(diǎn)內(nèi)還含有多個(gè)節(jié)點(diǎn)的xml
這篇文章主要介紹了使用dom4j遞歸解析節(jié)點(diǎn)內(nèi)還含有多個(gè)節(jié)點(diǎn)的xml,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09