第三十三條 用EnumMap代替序數索引
之前提過,我們可能會用ordinal()方法來求索引,代替一些陣列的索引,例如數種的例子,表示一些植物的歸類
public class Herb { public enum Type { ANNUAL, PERENNIAL, BIENNIAL } private final String name; private final Type type; Herb(String name, Type type) { this.name = name; this.type = type; } @Override public String toString() { return name; } }
假設植物是按照一年 兩年 多年 植物進行羅列,那麼需要三個集合,每個集合裝自己對應的植物,把植物園中所有的植物便利一遍,按照要求放進去,書中程式碼
// 我新增的,假設有四種植物,分別是 aa bb cc dd ,對應的的是列舉的三個型別值,分別代表 一年 兩年 多年,最終列印的 // 是 一年植物的集合 二年植物的集合 多年植物的集合 Herb[] garden = {new Herb("aa", Herb.Type.ANNUAL), new Herb("bb", Herb.Type.PERENNIAL), new Herb ("cc", Herb.Type.BIENNIAL), new Herb("dd", Herb.Type.ANNUAL)};
Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length]; for(int i=0; i < herbsByType.length; i++) herbsByType[i] = new HashSet<Herb>(); for(Herb h : garden) herbsByType[h.type.ordinal()].add(h); for(int i=0; i < herbsByType.length; i++) System.out.printf("%s: %s%n", Herb.Type.values()[i], herbsByType[i]);
上述程式碼沒問題,但有個可能出錯的地方就是泛型與陣列公用,我們知道,編譯時泛型生效,執行時擦除泛型,如果型別錯誤的話,會報錯的,這裡比較好的是,我們沒用錯,那就解讀一下這幾行程式碼吧。我們建立了個Set集合型別的陣列,就是說herbsByType[]陣列中的每個物件都是個Set集合,並且陣列中元素的個數是Herb.Type.values().length,值為3;然後便利這個陣列,往這個數組裡新增Set集合物件,此時 herbsByType[i] = new HashSet<Herb>(); 每個Set集合都是空集合;重點來了,整個程式碼中第二個增強for迴圈,便利了植物園的植物,也就是上面寫的garden陣列,有4個物件,拿第一個物件舉例,Herb a = new Herb("aa",Herb.Type.ANNUAL);增強for迴圈中 h 就是 Herb a,herbsByType[h.type.ordinal()].add(h); 先看h.type.ordinal(),我們知道意思就是Herb("aa", Herb.Type.ANNUAL)中的type,看最上面的程式碼,type即為Herb中的列舉Type,也就是Herb.Type.ANNUAL的ordinal值,那麼ANNUAL是列舉中的第一個,ordinal值為0,看到這,終於明白了,herbsByType[h.type.ordinal()].add(h); 就是說 herbsByType[0].add(a),即herbsByType[0].add(new Herb("aa", Herb.Type.ANNUAL)),然後增強for迴圈還有後面三個物件,同理,herbsByType[1].add(new Herb("bb", Herb.Type.PERENNIAL)),herbsByType[2].add(new Herb("cc",Herb.Type.BIENNIAL)),herbsByType[0].add(new Herb("aa", Herb.Type.ANNUAL))。我們繼續往下看,看第三個增強for,意思是遍歷herbsByType[]陣列,Herb.Type.values()[i] 意思依次列印 Herb 列舉的物件,herbsByType[i] 意思是對應的Set集合,Set集合中裝的是Herb列舉,列舉重寫了toString()方法,列印的是name,即 aa bb cc dd。最終列印結果符合咱們的預期: ANNUAL: [dd, aa] PERENNIAL: [bb] BIENNIAL: [cc]
上述分析了一堆,看起來也比較麻煩,書中給出了另外一種寫法。之前介紹了EnumSet,屬於Set集合;今天介紹EnumMap,當然屬於Map集合。
Herb[] garden = {new Herb("aa", Herb.Type.ANNUAL), new Herb("bb", Herb.Type.PERENNIAL), new Herb ("cc", Herb.Type.BIENNIAL),new Herb("dd", Herb.Type.ANNUAL)}; Map<Herb.Type, Set<Herb>> herbByType = new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class); for(Herb.Type t : Herb.Type.values()) herbByType.put(t, new HashSet<Herb>()); for(Herb h : garden) herbByType.get(h.type).add(h); System.out.println(herbByType);
這種寫法,更安全,不必要泛型手動輸入索引計算等等,也沒有不安全轉換,意思和上面的寫法一樣,只是在這裡,用了Map集合代替了之前的陣列,上面程式碼是陣列中嵌套了Set集合,陣列用索引來區分植物的年限;這裡是用Map集合,key值為植物年限,value值為Set集合,除此之外,其他的都一樣,列印結果為 {ANNUAL=[dd, aa], PERENNIAL=[bb], BIENNIAL=[cc]}
EnumMap 是屬於Map集合,class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>, 看到這,我們回想之前的EnumSet,就會發現用法有異曲同工之妙,只要熟悉了Set和Map,EnumSet和EnumMap的用法也就差不多了,無外乎多了點私有的api和屬性,並且記住,集合比陣列安全。
如果我們碰到兩次使用序數索引的陣列,即二維陣列,怎麼變,看下面例子
public enum Phase { SOLID, LIQUID, GAS; public enum Transition { MELT, FREEZE, BOTL, CONDENSE, SUBLIME, DEPOSIT; private static final Transition[][] TRANSITIONS = { {null, MELT, SUBLIME}, {FREEZE, null, BOTL}, {DEPOSIT, CONDENSE, null} }; public static Transition from(Phase src, Phase dst) { return TRANSITIONS[src.ordinal()][dst.ordinal()]; } } }
這段程式碼的意思是說,要進行二維陣列組合,通過方法去取對應的值,看起來沒問題,但實際上如果和第一個案例差不多,並且如果新增想新的植物,擴充套件不太方便,同理,可以使用EnumMap來修改,
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 src; private final Phase dst; Transition(Phase src, Phase dst) { this.src = src; this.dst = dst; } private static final Map<Phase, Map<Phase, Transition>> m = new EnumMap<Phase, Map<Phase, Transition>>(Phase.class); static { for(Phase p : Phase.values()) m.put(p, new EnumMap<Phase, Transition>(Phase.class)); for(Transition t : Transition.values()) m.get(t.src).put(t.dst, t); } public static Transition from(Phase src, Phase dst) { return m.get(src).get(dst); } } }
這種修改方法,有點類似第二十九條中的泛型案例,把可能出現的情況,全都列舉了出來,然後把他們裝進一個map集合,然後map集合巢狀集合,這個好處就是避免了陣列,不用擔心型別異常和資料越界等問題。在列舉中,也要記得,優先使用EnumMap和EnumSet,避免使用陣列。