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