Effective Java 第三版——37. 使用EnumMap替代序數索引

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java

Effective Java, Third Edition

37. 使用EnumMap替代序數索引

有時可能會看到使用ordinal方法(條目 35)來索引到數組或列表的代碼。 例如,考慮一下這個簡單的類來表明一種植物:程序員

class Plant {
    enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
    final String name;
    final LifeCycle lifeCycle;

    Plant(String name, LifeCycle lifeCycle) {
        [this.name](http://this.name) = name;
        this.lifeCycle = lifeCycle;
    }

    @Override public String toString() {
        return name;
    }
}

如今假設你有一組植物表明一個花園,想要列出這些由生命週期組織的植物(一年生,多年生,或雙年生)。爲此,須要構建三個集合,每一個生命週期做爲一個,並遍歷整個花園,將每一個植物放置在適當的集合中。一些程序員能夠經過將這些集合放入一個由生命週期序數索引的數組中來實現這一點:數組

// Using ordinal() to index into an array - DON'T DO THIS!

Set<Plant>[] plantsByLifeCycle =

    (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];

for (int i = 0; i < plantsByLifeCycle.length; i++)

    plantsByLifeCycle[i] = new HashSet<>();

for (Plant p : garden)

    plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);

// Print the results

for (int i = 0; i < plantsByLifeCycle.length; i++) {

    System.out.printf("%s: %s%n",

        Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);

}

這種方法是有效的,但充滿了問題。 由於數組不兼容泛型(條目 28),程序須要一個未經檢查的轉換,而且不會乾淨地編譯。 因爲該數組不知道索引表明什麼,所以必須手動標記索引輸出。 可是這種技術最嚴重的問題是,當你訪問一個由枚舉序數索引的數組時,你有責任使用正確的int值; int不提供枚舉的類型安全性。 若是你使用了錯誤的值,程序會默默地作錯誤的事情,若是你幸運的話,拋出一個ArrayIndexOutOfBoundsException異常。安全

有一個更好的方法來達到一樣的效果。 該數組有效地用做從枚舉到值的映射,所以不妨使用Map。 更具體地說,有一個很是快速的Map實現,設計用於枚舉鍵,稱爲java.util.EnumMap。 下面是當程序重寫爲使用EnumMap時的樣子:app

// Using an EnumMap to associate data with an enum

Map<Plant.LifeCycle, Set<Plant>>  plantsByLifeCycle =

    new EnumMap<>(Plant.LifeCycle.class);

for (Plant.LifeCycle lc : Plant.LifeCycle.values())

    plantsByLifeCycle.put(lc, new HashSet<>());

for (Plant p : garden)

    plantsByLifeCycle.get(p.lifeCycle).add(p);

System.out.println(plantsByLifeCycle);

這段程序更簡短,更清晰,更安全,運行速度與原始版本至關。 沒有不安全的轉換; 無需手動標記輸出,由於map鍵是知道如何將本身轉換爲可打印字符串的枚舉; 而且不可能在計算數組索引時出錯。 EnumMap與序數索引數組的速度至關,其緣由是EnumMap內部使用了這樣一個數組,但它對程序員的隱藏了這個實現細節,將Map的豐富性和類型安全性與數組的速度相結合。 請注意,EnumMap構造方法接受鍵類型的Class對象:這是一個有限定的類型令牌(bounded type token),它提供運行時的泛型類型信息(條目 33)。ide

經過使用stream(條目 45)來管理Map,能夠進一步縮短之前的程序。 如下是最簡單的基於stream的代碼,它們在很大程度上重複了前面示例的行爲:性能

// Naive stream-based approach - unlikely to produce an EnumMap!

System.out.println(Arrays.stream(garden)

        .collect(groupingBy(p -> p.lifeCycle)));

這個代碼的問題在於它選擇了本身的Map實現,實際上它不是EnumMap,因此它不會與顯式EnumMap的版本的空間和時間性能相匹配。 爲了解決這個問題,使用Collectors.groupingBy的三個參數形式的方法,它容許調用者使用mapFactory參數指定map的實現:學習

// Using a stream and an EnumMap to associate data with an enum

System.out.println(Arrays.stream(garden)

        .collect(groupingBy(p -> p.lifeCycle,

() -> new EnumMap<>(LifeCycle.class), toSet())));

這樣的優化在像這樣的示例程序中是不值得的,可是在大量使用Map的程序中多是相當重要的。優化

基於stream版本的行爲與EmumMap版本的行爲略有不一樣。 EnumMap版本老是爲每一個工廠生命週期生成一個嵌套map類,而若是花園包含一個或多個具備該生命週期的植物時,則基於流的版本纔會生成嵌套map類。 所以,例如,若是花園包含一年生和多年生植物但沒有兩年生的植物,plantByLifeCycle的大小在EnumMap版本中爲三個,在兩個基於流的版本中爲兩個。this

你可能會看到數組索引(兩次)的數組,用序數來表示從兩個枚舉值的映射。例如,這個程序使用這樣一個數組來映射兩個階段到一個階段轉換(phase transition)(液體到固體表示凝固,液體到氣體表示沸騰等等):

// Using ordinal() to index array of arrays - DON'T DO THIS!

public enum Phase {

    SOLID, LIQUID, GAS;

    public enum Transition {

        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

        // Rows indexed by from-ordinal, cols by to-ordinal

        private static final Transition[][] TRANSITIONS = {

            { null,    MELT,     SUBLIME },

            { FREEZE,  null,     BOIL    },

            { DEPOSIT, CONDENSE, null    }

        };

        // Returns the phase transition from one phase to another

        public static Transition from(Phase from, Phase to) {

            return TRANSITIONS[from.ordinal()][to.ordinal()];

        }

    }

}

這段程序能夠運行,甚至可能顯得優雅,但外觀多是騙人的。 就像前面顯示的簡單的花園示例同樣,編譯器沒法知道序數和數組索引之間的關係。 若是在轉換表中出錯或者在修改PhasePhase.Transition枚舉類型時忘記更新它,則程序在運行時將失敗。 失敗多是ArrayIndexOutOfBoundsExceptionNullPointerException或(更糟糕的)沉默無提示的錯誤行爲。 即便非空條目的數量較小,表格的大小也是phase的個數的平方。

一樣,能夠用EnumMap作得更好。 由於每一個階段轉換都由一對階段枚舉來索引,因此最好將關係表示爲從一個枚舉(from 階段)到第二個枚舉(to階段)到結果(階段轉換)的map。 與階段轉換相關的兩個階段最好經過將它們與階段轉換枚舉相關聯來捕獲,而後能夠用它來初始化嵌套的EnumMap

// Using a nested EnumMap to associate data with enum pairs

public enum Phase {

   SOLID, LIQUID, GAS;

   public enum Transition {

      MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),

      BOIL(LIQUID, GAS),   CONDENSE(GAS, LIQUID),

      SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

      private final Phase from;

      private final Phase to;

      Transition(Phase from, Phase to) {

         this.from = from;

         [this.to](http://this.to) = to;

      }

      // Initialize the phase transition map

      private static final Map<Phase, Map<Phase, Transition>>

        m = Stream.of(values()).collect(groupingBy(t -> t.from,

         () -> new EnumMap<>(Phase.class),

         toMap(t -> [t.to](http://t.to), t -> t,

            (x, y) -> y, () -> new EnumMap<>(Phase.class))));

      public static Transition from(Phase from, Phase to) {

         return m.get(from).get(to);

      }

   }

}

初始化階段轉換的map的代碼有點複雜。map的類型是Map<Phase, Map<Phase, Transition>>,意思是「從(源)階段映射到從(目標)階段到階段轉換映射。」這個map的map使用兩個收集器的級聯序列進行初始化。 第一個收集器按源階段對轉換進行分組,第二個收集器使用從目標階段到轉換的映射建立一個EnumMap。 第二個收集器((x, y) -> y))中的合併方法未使用;僅僅由於咱們須要指定一個map工廠才能得到一個EnumMap,而且Collectors提供伸縮式工廠,這是必需的。 本書的前一版使用顯式迭代來初始化階段轉換map。 代碼更詳細,但能夠更容易理解。

如今假設想爲系統添加一個新階段:等離子體或電離氣體。 這個階段只有兩個轉變:電離,將氣體轉化爲等離子體; 和去離子,將等離子體轉化爲氣體。 要更新基於數組的程序,必須將一個新的常量添加到Phase,將兩個兩次添加到Phase.Transition,並用新的十六個元素版本替換原始的九元素陣列數組。 若是向數組中添加太多或太少的元素或者將元素亂序放置,那麼若是運氣不佳:程序將會編譯,但在運行時會失敗。 要更新基於EnumMap的版本,只需將PLASMA添加到階段列表中,並將IONIZE(GAS, PLASMA)DEIONIZE(PLASMA, GAS)添加到階段轉換列表中:

// Adding a new phase using the nested EnumMap implementation

public enum Phase {

    SOLID, LIQUID, GAS, PLASMA;

    public enum Transition {

        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),

        BOIL(LIQUID, GAS),   CONDENSE(GAS, LIQUID),

        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),

        IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);

        ... // Remainder unchanged

    }

}

該程序會處理全部其餘事情,而且幾乎不會出現錯誤。 在內部,map的map是經過數組的數組實現的,所以在空間或時間上花費不多,以增長清晰度,安全性和易於維護。

爲了簡便起見,上面的示例使用null來表示狀態更改的缺失(其從目標到源都是相同的)。這不是很好的實踐,極可能在運行時致使NullPointerException。爲這個問題設計一個乾淨、優雅的解決方案是很是棘手的,並且結果程序足夠長,以致於它們會偏離這個條目的主要內容。

總之,使用序數來索引數組很不合適:改用EnumMap。 若是你所表明的關係是多維的,請使用EnumMap <...,EnumMap <... >>。 應用程序員應該不多使用Enum.ordinal(條目 35),若是使用了,也是通常原則的特例。

相關文章
相關標籤/搜索