1. 介紹
在 Java 中,有專為處理列舉(enum)而設計的集合:EnumSet 與 EnumMap。它們屬於標準程式庫(java.util),旨在讓 enum 型別的操作達到最高效率。
EnumSet 與 EnumMap 的內部如何運作?
EnumSet 的運作類似位元遮罩。可以想像是一組開關,每個列舉元素正好佔用一個位元。若位元為 1 — 表示該元素存在於集合中;若為 0 — 則不存在。所有資料都存放在數值陣列(long[])中;若你的 enum 少於 64 個值,整個集合只需要一個數字就能表示!
EnumMap 更為簡單:它是值的陣列,索引由 enum 元素的序號(ordinal)提供。相較於傳統的 HashMap<Enum, V>,你會得到更加精簡且快速的結構。
這帶來什麼好處?新增、刪除與檢查等操作皆為 O(1)。記憶體用量極低(尤其與 HashSet 與 HashMap 相比)。元素的遍歷順序永遠依照在 enum 中的宣告順序,因此結果可預期。
範例:實際長什麼樣
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
EnumSet<Day> weekend = EnumSet.of(Day.SAT, Day.SUN);
System.out.println(weekend); // [SAT, SUN]
從外觀上看,它就是一般的集合。但在內部,實際上是一個數字,且其中有兩個位元被設為 1:一個對應 SAT,另一個對應 SUN。若再加入 FRI,就會再開啟一個位元。完全不需要雜湊表或多餘的物件。
EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
schedule.put(Day.MON, "Gym");
schedule.put(Day.FRI, "Party");
System.out.println(schedule); // {MON=Gym, FRI=Party}
在這裡,鍵(Day)會轉換為陣列的內部索引,因此存取速度與陣列元素存取一樣快。
2. 使用情境:旗標、對應表、有限狀態機
EnumSet:非常適合旗標與狀態集合
- 旗標:針對有限數量的選項,儲存「啟用/停用」集合。
- 列舉值的集合:星期幾、使用者權限、任務狀態。
- 有限狀態機(FSM):可將允許的轉移儲存在 EnumSet 中。
範例:存取權限旗標
enum Permission { READ, WRITE, EXECUTE }
EnumSet<Permission> perms = EnumSet.of(Permission.READ, Permission.WRITE);
if (perms.contains(Permission.WRITE)) {
// 允許寫入
}
範例:除了某些值之外的全部值
EnumSet<Day> workdays = EnumSet.complementOf(EnumSet.of(Day.SAT, Day.SUN));
System.out.println(workdays); // [MON, TUE, WED, THU, FRI]
EnumMap:非常適合以 enum 為鍵的對應表
- 對應表:鍵是 enum 值,值可以是任意物件。
- 快速存取:比 HashMap<Enum, V> 更快且更精簡。
範例:依星期設定價格
EnumMap<Day, Integer> prices = new EnumMap<>(Day.class);
prices.put(Day.MON, 100);
prices.put(Day.SAT, 200);
System.out.println(prices.get(Day.SAT)); // 200
範例:有限狀態機
enum State { START, RUNNING, STOPPED }
EnumMap<State, EnumSet<State>> transitions = new EnumMap<>(State.class);
transitions.put(State.START, EnumSet.of(State.RUNNING));
transitions.put(State.RUNNING, EnumSet.of(State.STOPPED));
transitions.put(State.STOPPED, EnumSet.noneOf(State.class));
3. 潛在陷阱:enum 變更與序列化
EnumSet 與 EnumMap 依賴於你的 enum 的成員與其順序。若你新增新元素、刪除舊元素,或變更其順序,先前保存或序列化的集合可能會表現不正確。
序列化
- EnumSet 與 EnumMap 可被序列化,但若在序列化與還原之間 enum 發生變更,可能導致錯誤或資料遺失。
- 在序列化之後刪除 enum 的某個值,幾乎必然會在讀取時拋出例外。
最佳實務:
- 若無法保證 enum 穩定,請不要序列化 EnumSet/EnumMap。
- 若需長期保存,可改用例如值的字串表示清單。
4. 實用細節
EnumSet/EnumMap 與一般集合的比較
| 集合 | 鍵/元素 | 內部實作 | 效能 | 記憶體 | Null |
|---|---|---|---|---|---|
|
|
位元遮罩 | O(1) | 極少 | 不支援 |
|
|
雜湊表 | O(1) | 較多 | 可用 |
|
|
依 ordinal 的陣列 | O(1) | 極少 | 不支援 |
|
|
雜湊表 | O(1) | 較多 | 可用 |
最佳實務
- 在值集合情境使用 EnumSet(of、noneOf、allOf、complementOf)。
- 當關聯表的鍵為 enum 時,使用 EnumMap。
- 避免「龐大的」列舉——包含數百個值的清單會降低緊湊度。
- 若 enum 可能變更,請不要序列化。
- 不要使用 null 作為鍵或值。
5. 實作:在應用程式中如何使用 EnumSet 與 EnumMap
範例:儲存使用者角色
enum Role { USER, ADMIN, MODERATOR }
class User {
private EnumSet<Role> roles = EnumSet.noneOf(Role.class);
public void addRole(Role role) {
roles.add(role);
}
public boolean isAdmin() {
return roles.contains(Role.ADMIN);
}
}
範例:狀態轉移表
enum State { NEW, IN_PROGRESS, DONE }
EnumMap<State, EnumSet<State>> transitions = new EnumMap<>(State.class);
transitions.put(State.NEW, EnumSet.of(State.IN_PROGRESS));
transitions.put(State.IN_PROGRESS, EnumSet.of(State.DONE));
transitions.put(State.DONE, EnumSet.noneOf(State.class));
6. 使用 EnumSet/EnumMap 時的常見錯誤
錯誤 № 1:將 EnumSet/EnumMap 用於非 enum 型別。
這些集合只適用於屬於 enum 的型別。
// EnumSet<String> set = EnumSet.of("A", "B"); // 編譯錯誤!
錯誤 № 2:對非常龐大的 enum 使用 EnumSet/EnumMap。
包含數百個值的列舉會降低緊湊度。然而,對相同資料而言,這仍往往比 HashSet/HashMap 更省記憶體。
錯誤 № 3:在序列化之後變更 enum。
在序列化之後新增/刪除/重新排序列舉值,會導致讀取時發生錯誤或資料遺失。
錯誤 № 4:在 EnumSet/EnumMap 中使用 null。
無論是 EnumSet 的元素,或 EnumMap 的鍵/值,都不可為 null。
EnumSet<Day> days = EnumSet.of(null); // NullPointerException!
EnumMap<Day, String> map = new EnumMap<>(Day.class);
map.put(null, "test"); // NullPointerException!
錯誤 № 5:以為 EnumSet 是「一般」的 Set。
EnumSet 只會儲存其對應 enum 的值;你無法加入列舉之外的任意物件。
錯誤 № 6:將 EnumSet/EnumMap 用於「可變」的 enum。
若列舉在執行階段被生成或替換(動態載入/反射),這些結構將無法正確運作。
GO TO FULL VERSION