整理Effective Java書中Item 37: Use EnumMap instead of ordinal indexing心得筆記

主旨

在 Java 中,我們有時會用 int 索引的陣列來儲存與 enum 相關的資料,但這種做法容易造成型別不安全和可讀性問題。Java 提供了 EnumMap 類別,專門用來處理 enum 類型的映射關係,提供更安全、更清楚的解決方案。

問題:int 索引陣列的缺點

假設我們要儲存每個星期幾的開門時間,傳統作法可能會這樣寫:

public class StoreHours {
    private static final int[] OPENING_HOURS = {
        9,  // Monday
        9,  // Tuesday
        9,  // Wednesday
        9,  // Thursday
        9,  // Friday
        10, // Saturday
        10  // Sunday
    };

    public int getOpeningHour(Day day) {
        return OPENING_HOURS[day.ordinal()];
    }
}

這種寫法有幾個問題:

  1. 型別不安全:OPENING_HOURS 可以接受任何整數值
  2. 可讀性差:必須靠註解來說明每個索引的意義
  3. 維護成本高:如果 enum 新增或移除元素,陣列大小也要跟著改

解決方案:使用 EnumMap

EnumMap 是專門為 enum 設計的 Map 實作,它比一般 HashMap 更有效率,因為它知道 key 的數量是固定的。

public class StoreHours {
    private final EnumMap<Day, Integer> openingHours = new EnumMap<>(Day.class);

    public StoreHours() {
        openingHours.put(Day.MONDAY, 9);
        openingHours.put(Day.TUESDAY, 9);
        openingHours.put(Day.WEDNESDAY, 9);
        openingHours.put(Day.THURSDAY, 9);
        openingHours.put(Day.FRIDAY, 9);
        openingHours.put(Day.SATURDAY, 10);
        openingHours.put(Day.SUNDAY, 10);
    }

    public int getOpeningHour(Day day) {
        return openingHours.get(day);
    }
}

使用 EnumMap 的好處:

  1. 型別安全:只能接受 Day 類型的 key
  2. 可讀性高:直接用 enum 值做 key,不用靠註解
  3. 維護容易:新增或移除 enum 元素時,EnumMap 會自動處理
  4. 效能好:因為 key 數量固定,所以比 HashMap 更有效率

實作範例

假設我們要建立一個應用程式設定系統,用於管理系統的各種配置選項:

public enum ConfigKey {
    MAX_CONNECTIONS, TIMEOUT, CACHE_SIZE, LOG_LEVEL, MAX_THREADS
}

public class ConfigurationSettings {
    private final EnumMap<ConfigKey, Object> settings = new EnumMap<>(ConfigKey.class);

    public ConfigurationSettings() {
        // 預設設定值
        settings.put(ConfigKey.MAX_CONNECTIONS, 100);
        settings.put(ConfigKey.TIMEOUT, 5000);
        settings.put(ConfigKey.CACHE_SIZE, 1024);
        settings.put(ConfigKey.LOG_LEVEL, "INFO");
        settings.put(ConfigKey.MAX_THREADS, 20);
    }

    // 設定值方法
    public void setSetting(ConfigKey key, Object value) {
        settings.put(key, value);
    }

    // 取得設定值方法
    @SuppressWarnings("unchecked")
    public <T> T getSetting(ConfigKey key) {
        return (T) settings.get(key);
    }

    // 驗證設定值的範圍
    public boolean validateSettings() {
        int maxConnections = getSetting(ConfigKey.MAX_CONNECTIONS);
        if (maxConnections < 1 || maxConnections > 1000) return false;
        
        int timeout = getSetting(ConfigKey.TIMEOUT);
        if (timeout < 1000 || timeout > 30000) return false;
        
        return true;
    }

    // 重置設定到預設值
    public void resetToDefaults() {
        settings.clear();
        // 重新設定預設值
        settings.put(ConfigKey.MAX_CONNECTIONS, 100);
        settings.put(ConfigKey.TIMEOUT, 5000);
        settings.put(ConfigKey.CACHE_SIZE, 1024);
        settings.put(ConfigKey.LOG_LEVEL, "INFO");
        settings.put(ConfigKey.MAX_THREADS, 20);
    }
}

// 使用方式
public class SystemManager {
    private final ConfigurationSettings config = new ConfigurationSettings();

    public void configureSystem() {
        // 設定系統參數
        config.setSetting(ConfigKey.MAX_CONNECTIONS, 200);
        config.setSetting(ConfigKey.TIMEOUT, 10000);
        config.setSetting(ConfigKey.CACHE_SIZE, 2048);
        config.setSetting(ConfigKey.LOG_LEVEL, "DEBUG");
        config.setSetting(ConfigKey.MAX_THREADS, 50);

        // 驗證設定
        if (!config.validateSettings()) {
            throw new IllegalArgumentException("Invalid configuration values");
        }

        // 取得設定值
        int maxThreads = config.getSetting(ConfigKey.MAX_THREADS);
        System.out.println("Maximum threads: " + maxThreads);
    }
}

小結

EnumMap 提供了比 int 索引陣列更安全、更清楚的解決方案。它特別適合用在需要將 enum 值映射到其他值的情況下,能讓程式碼更易讀、更不易出錯。下次當你需要儲存與 enum 相關的資料時,考慮使用 EnumMap 來取代傳統的 int 索引陣列吧!