Java編程思想 枚舉類型總結

關鍵字enum可以將一組具名的值的有限集合創建爲一種新的類型 而這些具名的值可以作爲常規的程序組件使用 這是一種非常有用的功能

基本enum特性
創建enum時 編譯器會爲你生成一個相關的類 這個類繼承自java.lang.Enum 下面的例子演示了Enum提供的一些功能
在這裏插入圖片描述
在這裏插入圖片描述

將靜態導入用於enum
Burrito.java的另一個版本
在這裏插入圖片描述
使用static import能夠將enum實例的標識符帶入當前的命名空間 所以無需再用enum類型來修飾enum實例 這是一個好的想法嗎 或者還是顯式地修飾enum實例更好 這要看代碼的複雜程度了 編譯器可以確保你使用的是正確的類型 所以唯一需要擔心的是 使用靜態導入會不會導致你的代碼令人難以理解 多數情況下 使用static import還是有好處的 不過 程序員還是應該對具體情況進行具體分析
注意 在定義enum的同一個文件中 這種技巧無法使用 如果是在默認包中定義enum 這種技巧也無法使用

向enum中添加新方法
除了不能繼承自一個enum之外 我們基本上可以將enum看作一個常規的類 也就是說 我們可以向enum中添加方法 enum甚至可以有main()方法
一般來說 我們希望每個枚舉實例能夠返回對自身的描述 而不僅僅只是默認的toString()實現 這隻能返回枚舉實例的名字 爲此 你可以提供一個構造器 專門負責處理這個額外的信息 然後添加一個方法 返回這個描述信息 看一看下面的示例
在這裏插入圖片描述
注意 如果你打算定義自己的方法 那麼必須在enum實例序列的最後添加一個分號 同時 Java要求你必須先定義enum實例 如果在定義enum實例之前定義了任何方法或屬性 那麼在編譯時就會得到錯誤信息

覆蓋enum的方法
覆蓋toString()方法 給我們提供了另一種方式來爲枚舉實例生成不同的字符串描述信息 在下面的示例中 我們使用的就是實例的名字 不過我們希望改變其格式 覆蓋enum的toSting()方法與覆蓋一般類的方法沒有區別
在這裏插入圖片描述
在這裏插入圖片描述

switch語句中的enum
在switch中使用enum 是enum提供的一項非常便利的功能 一般來說 在switch中只能使用整數值 而枚舉實例天生就具備整數值的次序 並且可以通過ordinal()方法取得其次序(顯然編譯器幫我們做了類似的工作) 因此我們可以在switch語句中使用enum
雖然一般情況下我們必須使用enum類型來修飾一個enum實例 但是在case語句中卻不必如此 下面的例子使用enum構造了一個小型狀態機
在這裏插入圖片描述

values()的神祕之處
編譯器爲你創建的enum類都繼承自Enum類 然而 如果你研究一下Enum類就會發現 它並沒有values()方法 可我們明明已經用過該方法了 難道存在某種 隱藏的 方法嗎 我們可以利用反射機制編寫一個簡單的程序 來查看其中的究竟
在這裏插入圖片描述
在這裏插入圖片描述
答案是 values()是由編譯器添加的static方法 可以看出 在創建Explore的過程中 編譯器還爲其添加了valueOf()方法 這可能有點令人迷惑 Enum類不是已經有valueOf()方法了嗎 不過Enum中的valueOf()方法需要兩個參數 而這個新增的方法只需一個參數 由於這裏使用的Set只存儲方法的名字 而不考慮方法的簽名 所以在調用Explore.removeAll(Enum)之後 就只剩下[values]了

由於values()方法是由編譯器插入到enum定義中的static方法 所以 如果你將enum實例向上轉型爲Enum 那麼values()方法就不可訪問了 不過 在Class中有一個getEnumConstants()方法 所以即便Enum接口中沒有values()方法 我們仍然可以通過Class對象取得所有enum實例
在這裏插入圖片描述
因爲getEnumConstants()是Class上的方法 所以你甚至可以對不是枚舉的類調用此方法
在這裏插入圖片描述
只不過 此時該方法返回null 所以當你試圖使用其返回的結果時會發生異常

實現 而非繼承
我們已經知道 所有的enum都繼承自java.lang.Enum類 由於Java不支持多重繼承 所以你的enum不能再繼承其他類
在這裏插入圖片描述
然而 在我們創建一個新的enum時 可以同時實現一個或多個接口
在這裏插入圖片描述
這個結果有點奇怪 不過你必須要有一個enum實例才能調用其上的方法 現在 在任何接受Generator參數的方法中 例如printNext() 都可以使用CartoonCharacter

隨機選取
就像你在CartoonCharacter.next()中看到的那樣 本章中的很多示例都需要從enum實例中進行隨機選擇 我們可以利用泛型 從而使得這個工作更一般化 並將其加入到我們的工具庫中
在這裏插入圖片描述
在這裏插入圖片描述
古怪的語法<T extends Enum>表示T是一個enum實例 而將Class作爲參數的話 我們就可以利用Class對象得到enum實例的數組了 重載後的random()方法只需使用T[]作爲參數 因爲它並不會調用Enum上的任何操作 它只需從數組中隨機選擇一個元素即可 這樣 最終的返回類型正是enum的類型
下面是random()方法的一個簡單示例
在這裏插入圖片描述
雖然Enum只是一個相當短小的類 但是你會發現 它能消除很多重複的代碼 重複總會製造麻煩 因此消除重複總是有益處的

使用接口組織枚舉
無法從enum繼承子類有時很令人沮喪 這種需求有時源自我們希望擴展原enum中的元素 有時是因爲我們希望使用子類將一個enum中的元素進行分組
在一個接口的內部 創建實現該接口的枚舉 以此將元素進行分組 可以達到將枚舉元素分類組織的目的 舉例來說 假設你想用enum來表示不同類別的食物 同時還希望每個enum元素仍然保持Food類型 那可以這樣實現
在這裏插入圖片描述
對於enum而言 實現接口是使其子類化的唯一辦法 所以嵌入在Food中的每個enum都實現了Food接口 現在 在下面的程序中 我們可以說 所有東西都是某種類型的Food
在這裏插入圖片描述
如果enum類型實現了Food接口 那麼我們就可以將其實例向上轉型爲Food 所以上例中的所有東西都是Food
然而 當你需要與一大堆類型打交道時 接口就不如enum好用了 例如 如果你想創建一個 枚舉的枚舉 那麼可以創建一個新的enum 然後用其實例包裝Food中的每一個enum類
在這裏插入圖片描述
在上面的程序中 每一個Course的實例都將其對應的Class對象作爲構造器的參數 通過getEnumConstants()方法 可以從該Class對象中取得某個Food子類的所有enum實例 這些實例在randomSelection()中被用到 因此 通過從每一個Course實例中隨機地選擇一個Food 我們便能夠生成一份菜單
在這裏插入圖片描述
在這裏插入圖片描述
在這個例子中 我們通過遍歷每一個Course實例來獲得 枚舉的枚舉 的值 稍後 在VendingMachine.java中 我們會看到另一種組織枚舉實例的方式 但其也有一些其他的限制
此外 還有一種更簡潔的管理枚舉的辦法 就是將一個enum嵌套在另一個enum內 就像這樣
在這裏插入圖片描述
Security接口的作用是將其所包含的enum組合成一個公共類型 這一點是有必要的 然後 SecurityCategory才能將Security中的enum作爲其構造器的參數使用 以起到組織的效果
如果我們將這種方式應用於Food的例子 結果應該這樣
在這裏插入圖片描述
其實 這僅僅是重新組織了一下代碼 不過多數情況下 這種方式使你的代碼具有更清晰的結構

使用EnumSet替代標誌
Set是一種集合 只能向其中添加不重複的對象 當然 enum也要求其成員都是唯一的 所以enum看起來也具有集合的行爲 不過 由於不能從enum中刪除或添加元素 所以它只能算是不太有用的集合 Java SE5引入EnumSet 是爲了通過enum創建一種替代品 以替代傳統的基於int的 位標誌 這種標誌可以用來表示某種 開/關 信息 不過 使用這種標誌 我們最終操作的只是一些bit 而不是這些 bit想要表達的概念 因此很容易寫出令人難以理解的代碼

EnumSet中的元素必須來自一個enum 下面的enum表示在一座大樓中 警報傳感器的安放位置
在這裏插入圖片描述
然後 我們用EnumSet來跟蹤報警器的狀態
在這裏插入圖片描述

EnumSet的基礎是long 一個long值有64位 而一個enum實例只需一位bit表示其是否存在 也就是說 在不超過一個long的表達能力的情況下 你的EnumSet可以應用於最多不超過64個元素的enum 如果enum超過了64個元素會發生什麼呢
在這裏插入圖片描述
顯然 EnumSet可以應用於多過64個元素的enum 所以猜測 Enum會在必要的時候增加一個long

使用EnumMap
EnumMap是一種特殊的Map 它要求其中的鍵(key)必須來自一個enum 由於enum本身的限制 所以EnumMap在內部可由數組實現 因此EnumMap的速度很快 我們可以放心地使用enum實例在EnumMap中進行查找操作 不過 我們只能將enum的實例作爲鍵來調用put()方法 其他操作與使用一般的Map差不多
下面的例子演示了命令設計模式的用法 一般來說 命令模式首先需要一個只有單一方法的接口 然後從該接口實現具有各自不同的行爲的多個子類 接下來 程序員就可以構造命令對象 並在需要的時候使用它們了
在這裏插入圖片描述
在這裏插入圖片描述
與EnumSet一樣 enum實例定義時的次序決定了其在EnumMap中的順序
main()方法的最後部分說明 enum的每個實例作爲一個鍵 總是存在的 但是 如果你沒有爲這個鍵調用put()方法來存入相應的值的話 其對應的值就是null
與常量相關的方法(constant specific methods)相比 EnumMap有一個優點 那EnumMap允許程序員改變值對象 而常量相關的方法在編譯期就被固定了
稍後你會看到 在你有多種類型enum 而且它們之間存在互操作的情況下 我們可以用EnumMap實現多路分發(multiple dispatching)

常量相關的方法
Java的enum有一個非常有趣的特性 即它允許程序員爲enum實例編寫方法 從而爲每個enum實例賦予各自不同的行爲 要實現常量相關的方法 你需要爲enum定義一個或多個abstract方法 然後爲每個enum實例實現該抽象方法 參考下面的例子
在這裏插入圖片描述
通過相應的enum實例 我們可以調用其上的方法 這通常也稱爲表驅動的代碼(table driven code 請注意它與前面提到的命令模式的相似之處)
在面向對象的程序設計中 不同的行爲與不同的類關聯 而通過常量相關的方法 每個enum實例可以具備自己獨特的行爲 這似乎說明每個enum實例就像一個獨特的類 在上面的例子中 enum實例似乎被當作其 超類 ConstantSpecificMethod來使用 在調用getInfo()方法時 體現出多態的行爲
然而 enum實例與類的相似之處也僅限於此了 我們並不能真的將enum實例作爲一個類型來使用
在這裏插入圖片描述

再看一個更有趣的關於洗車的例子 每個顧客在洗車時 都有一個選擇菜單 每個選擇對應一個不同的動作 可以將一個常量相關的方法關聯到一個選擇上 再使用一個EnumSet來保存客戶的選擇
在這裏插入圖片描述
在這裏插入圖片描述
與使用匿名內部類相比較 定義常量相關方法的語法更高效 簡潔

除了實現abstract方法以外 程序員是否可以覆蓋常量相關的方法呢 答案是肯定的 參考下面的程序
在這裏插入圖片描述
在這裏插入圖片描述
雖然enum有某些限制 但是一般而言 我們還是可以將其看作是類

使用enum的職責鏈
在職責鏈(Chain of Responsibility)設計模式中 程序員以多種不同的方式來解決一個問題 然後將它們鏈接在一起 當一個請求到來時 它遍歷這個鏈 直到鏈中的某個解決方案能夠處理該請求
通過常量相關的方法 我們可以很容易得實現一個簡單的職責鏈 我們以一個郵局的模型爲例 郵局需要以儘可能通用的方式來處理每一封郵件 並且要不斷嘗試處理郵件 直到該郵件最終被確定爲一封死信 其中的每一次嘗試可以看作爲一個策略(也是一個設計模式) 而完整的處理方式列表就是一個職責鏈
我們先來描述一下郵件 郵件的每個關鍵特徵都可以用enum來表示 程序將隨機地生成Mail對象 如果要減小一封郵件的GeneralDelivery爲YES的概率 那最簡單的方法就是多創建幾個不是YES的enum實例 所以enum的定義看起來有點古怪
我們看到Mail中有一個randomMail()方法 它負責隨機地創建用於測試的郵件 而generator()方法生成一個Iterable對象 該對象在你調用next()方法時 在其內部使用randomMail()來創建Mail對象 這樣的結構使程序員可以通過調用Mail.generator()方法 很容易地構造出一個foreach循環
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
職責鏈由enum MailHandler實現 而enum定義的次序決定了 各個解決策略在應用時的次序 對每一封郵件 都要按此順序嘗試每個解決策略 直到其中一個能夠成功地處理該郵件 如果所有的策略都失敗了 那麼該郵件將被判定爲一封死信

使用enum的狀態機
枚舉類型非常適合用來創建狀態機 一個狀態機可以具有有限個特定的狀態 它通常根據輸入 從一個狀態轉移到下一個狀態 不過也可能存在瞬時狀態(transient states) 而一旦任務執行結束 狀態機就會立刻離開瞬時狀態
每個狀態都具有某些可接受的輸入 不同的輸入會使狀態機從當前狀態轉移到不同的新狀態 由於enum對其實例有嚴格限制 非常適合用來表現不同的狀態和輸入 一般而言 每個狀態都具有一些相關的輸出
自動售貨機是一個很好的狀態機的例子 首先 我們用一個enum定義各種輸入
在這裏插入圖片描述

VendingMachine對輸入的第一個反應是將其歸類爲Category enum中的某個enum實例 這可以通過switch實現 下面的例子演示了enum是如何使代碼變得更加清晰且易於管理的
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

通過兩種不同的Generator對象 我們可以用兩種方式來測試VendingMachine 首先是RandomInputGenerator 它會不停地生成各種輸入 當然 除了SHUT_DOWN之外 通過長時間地運行RandomInputGenerator 可以起到健全測試(sanity test)的作用 能夠確保該狀態機不會進入一個錯誤狀態 另一個是FileInputGenerator 使用文件以文本的方式來描述輸入 然後將它們轉換成enum實例 並創建對應的Input對象 上面的程序使用的正是如下的文本文件
在這裏插入圖片描述
這種設計有一個缺陷 它要求enum State實例訪問的VendingMachine屬性必須聲明爲static 這意味着 你只能有一個VendingMachine實例 不過如果我們思考一下實際的(嵌入式Java)應用 這也許並不是一個大問題 因爲在一臺機器上 我們可能只有一個應用程序

多路分發
當你要處理多種交互類型時 程序可能會變得相當雜亂 舉例來說 如果一個系統要分析和執行數學表達式 我們可能會聲明Number.plus(Number) Number.multiple(Number)等等 其中Number是各種數字對象的超類 然而 當你聲明a.plus(b)時 你並不知道a或b的確切類型 那你如何能讓它們正確地交互呢
你可能從未思考過這個問題的答案 Java只支持單路分發 也就是說 如果要執行的操作包含了不止一個類型未知的對象時 那麼Java的動態綁定機制只能處理其中一個的類型 這就無法解決我們上面提到的問題 所以 你必須自己來判定其他的類型 從而實現自己的動態綁定行爲
解決上面問題的辦法就是多路分發(在那個例子中 只有兩個分發 一般稱之爲兩路分發) 多態只能發生在方法調用時 所以 如果你想使用兩路分發 那麼就必須有兩個方法調用 第一個方法調用決定第一個未知類型 第二個方法調用決定第二個未知的類型 要利用多路分發 程序員必須爲每一個類型提供一個實際的方法調用 如果你要處理兩個不同的類型體系 就需要爲每個類型體系執行一個方法調用 一般而言 程序員需要有設定好的某種配置 以便一個方法調用能夠引出更多的方法調用 從而能夠在這個過程中處理多種類型 爲了達到這種效果 我們需要與多個方法一同工作 因爲每個分發都需要一個方法調用 在下面的例子中(實現了 石頭 剪刀 布 遊戲 也稱爲RoShamBo)對應的方法是compete()和eval() 二者都是同一個類型的成員 它們可以產生三種Outcome實例中的一個作爲結果
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

使用enum分發
直接將RoShamBo1.java翻譯爲基於enum的版本是有問題的 因爲enum實例不是類型 不能將enum實例作爲參數的類型 所以無法重載eval()方法 不過 還有很多方式可以實現多路分發 並從enum中獲益
一種方式是使用構造器來初始化每個enum實例 並以 一組 結果作爲參數 這二者放在一塊 形成了類似查詢表的結構
在這裏插入圖片描述
在這裏插入圖片描述

在代碼中 enum被單獨抽取出來 因此它可以應用在其他例子中 首先 Competitor接口定義了一種類型 該類型的對象可以與另一個Competitor相競爭
在這裏插入圖片描述
然後 我們定義兩個static方法(static可以避免顯式地指明參數類型) 第一個是match()方法 它會爲一個Competitor對象調用compete()方法 並與另一個Competitor對象作比較 在這個例子中 我們看到 match()方法的參數需要是Competitor類型 但是在play()方法中 類型參數必須同時是Enum類型(因爲它將在Enums.random()中使用)和Competitor類型(因爲它將被傳遞給match()方法)
在這裏插入圖片描述
play()方法沒有將類型參數T作爲返回值類型 因此 似乎我們應該在Class中使用通配符來代替上面的參數聲明 然而 通配符不能擴展多個基類 所以我們必須採用以上的表達式

使用常量相關的方法
常量相關的方法允許我們爲每個enum實例提供方法的不同實現 這使得常量相關的方法似乎是實現多路分發的完美解決方案 不過 通過這種方式 enum實例雖然可以具有不同的行爲 但它們仍然不是類型 不能將其作爲方法簽名中的參數類型來使用 最好的辦法是將enum用在switch語句中 見下例
在這裏插入圖片描述
雖然這種方式可以工作 但是卻不甚合理 如果採用RoShamBo2.java的解決方案 那麼在添加一個新的類型時 只需更少的代碼 而且也更直接
然而 RoShamBo3.java還可以壓縮簡化一下
在這裏插入圖片描述
在這裏插入圖片描述
其中 具有兩個參數的compete()方法執行第二個分發 該方法執行一系列的比較 其行爲類似switch語句 這個版本的程序更簡短 不過卻比較難理解 對於一個大型系統而言 難以理解的代碼將導致整個系統不夠健壯

使用EnumMap分發
使用EnumMap能夠實現 真正的 兩路分發 EnumMap是爲enum專門設計的一種性能非常好的特殊Map 由於我們的目的是摸索出兩種未知的類型 所以可以用一個EnumMap的EnumMap來實現兩路分發
在這裏插入圖片描述
該程序在一個static子句中初始化EnumMap對象 具體見表格似的initRow()方法調用 請注意compete()方法 您可以看到 在一行語句中發生了兩次分發

使用二維數組
我們還可以進一步簡化實現兩路分發的解決方案 我們注意到 每個enum實例都有一個固定的值(基於其聲明的次序) 並且可以通過ordinal()方法取得該值 因此我們可以使用二維數組 將競爭者映射到競爭結果 採用這種方式能夠獲得最簡潔 最直接的解決方案(很可能也是最快速的 雖然我們知道EnumMap內部其實也是使用數組實現的)
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章