CodeGym /課程 /JAVA 25 SELF /EnumSet/EnumMap

EnumSet/EnumMap

JAVA 25 SELF
等級 28 , 課堂 3
開放

1. 介紹

在 Java 中,有專為處理列舉(enum)而設計的集合:EnumSetEnumMap。它們屬於標準程式庫(java.util),旨在讓 enum 型別的操作達到最高效率。

EnumSet 與 EnumMap 的內部如何運作?

EnumSet 的運作類似位元遮罩。可以想像是一組開關,每個列舉元素正好佔用一個位元。若位元為 1 — 表示該元素存在於集合中;若為 0 — 則不存在。所有資料都存放在數值陣列(long[])中;若你的 enum 少於 64 個值,整個集合只需要一個數字就能表示!

EnumMap 更為簡單:它是值的陣列,索引由 enum 元素的序號(ordinal)提供。相較於傳統的 HashMap<Enum, V>,你會得到更加精簡且快速的結構。

這帶來什麼好處?新增、刪除與檢查等操作皆為 O(1)。記憶體用量極低(尤其與 HashSetHashMap 相比)。元素的遍歷順序永遠依照在 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 變更與序列化

EnumSetEnumMap 依賴於你的 enum 的成員與其順序。若你新增新元素、刪除舊元素,或變更其順序,先前保存或序列化的集合可能會表現不正確。

序列化

  • EnumSetEnumMap 可被序列化,但若在序列化與還原之間 enum 發生變更,可能導致錯誤或資料遺失。
  • 在序列化之後刪除 enum 的某個值,幾乎必然會在讀取時拋出例外。

最佳實務:

  • 若無法保證 enum 穩定,請不要序列化 EnumSet/EnumMap
  • 若需長期保存,可改用例如值的字串表示清單。

4. 實用細節

EnumSet/EnumMap 與一般集合的比較

集合 鍵/元素 內部實作 效能 記憶體 Null
EnumSet
enum
位元遮罩 O(1) 極少 不支援
HashSet<Enum>
enum
雜湊表 O(1) 較多 可用
EnumMap
enum
依 ordinal 的陣列 O(1) 極少 不支援
HashMap<Enum, V>
enum
雜湊表 O(1) 較多 可用

最佳實務

  • 在值集合情境使用 EnumSetofnoneOfallOfcomplementOf)。
  • 當關聯表的鍵為 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。
若列舉在執行階段被生成或替換(動態載入/反射),這些結構將無法正確運作。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION