Occasionally you may see code that uses the ordinal method (Item 35) to index into an array or list. For example, consider this simplistic class meant to represent a plant:
偶爾你可能會(huì)看到使用 ordinal()
的返回值(Item-35)作為數(shù)組或 list 索引的代碼坎背。例如窝剖,考慮這個(gè)簡(jiǎn)單的類(lèi)堪唐,它表示一種植物:
class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override public String toString() {
return name;
}
}
Now suppose you have an array of plants representing a garden, and you want to list these plants organized by life cycle (annual, perennial, or biennial). To do this, you construct three sets, one for each life cycle, and iterate through the garden, placing each plant in the appropriate set. Some programmers would do this by putting the sets into an array indexed by the life cycle’s ordinal:
現(xiàn)在假設(shè)你有一個(gè)代表花園全部植物的 Plant 數(shù)組朴读,你想要列出按生命周期(一年生似踱、多年生或兩年生)排列的植物盾舌。要做到這一點(diǎn)墓臭,你需要構(gòu)造三個(gè)集合,每個(gè)生命周期一個(gè)妖谴,然后遍歷整個(gè)數(shù)組窿锉,將每個(gè)植物放入適當(dāng)?shù)募现校?/p>
// 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]);
}
譯注:假設(shè) Plant 數(shù)組如下:
Plant[] garden = new Plant[]{
new Plant("A", LifeCycle.ANNUAL),
new Plant("B", LifeCycle.BIENNIAL),
new Plant("C", LifeCycle.PERENNIAL),
new Plant("D", LifeCycle.BIENNIAL),
new Plant("E", LifeCycle.PERENNIAL),
};
輸出結(jié)果為:
ANNUAL: [A]
PERENNIAL: [E, C]
BIENNIAL: [B, D]
This technique works, but it is fraught with problems. Because arrays are not compatible with generics (Item 28), the program requires an unchecked cast and will not compile cleanly. Because the array does not know what its index represents, you have to label the output manually. But the most serious problem with this technique is that when you access an array that is indexed by an enum’s ordinal, it is your responsibility to use the correct int value; ints do not provide the type safety of enums. If you use the wrong value, the program will silently do the wrong thing or—if you’re lucky—throw an ArrayIndexOutOfBoundsException.
這種技術(shù)是有效的,但它充滿了問(wèn)題膝舅。因?yàn)閿?shù)組與泛型不兼容(Item-28)嗡载,所以該程序需要 unchecked 的轉(zhuǎn)換,否則不能順利地編譯仍稀。因?yàn)閿?shù)組不知道它的索引表示什么洼滚,所以必須手動(dòng)標(biāo)記輸出。但是這種技術(shù)最嚴(yán)重的問(wèn)題是技潘,當(dāng)你訪問(wèn)一個(gè)由枚舉序數(shù)索引的數(shù)組時(shí)遥巴,你有責(zé)任使用正確的 int 值;int 不提供枚舉的類(lèi)型安全性享幽。如果你使用了錯(cuò)誤的值铲掐,程序?qū)㈧o默執(zhí)行錯(cuò)誤的操作,如果幸運(yùn)的話琉闪,才會(huì)拋出 ArrayIndexOutOfBoundsException迹炼。
There is a much better way to achieve the same effect. The array is effectively serving as a map from the enum to a value, so you might as well use a Map. More specifically, there is a very fast Map implementation designed for use with enum keys, known as java.util.EnumMap. Here is how the program looks when it is rewritten to use EnumMap:
有一種更好的方法可以達(dá)到同樣的效果。該數(shù)組有效地充當(dāng)從枚舉到值的映射颠毙,因此你不妨使用 Map斯入。更具體地說(shuō),有一種非持郏快速的 Map 實(shí)現(xiàn)刻两,用于枚舉鍵,稱(chēng)為 java.util.EnumMap
滴某。以下就是這個(gè)程序在使用 EnumMap 時(shí)的樣子:
// 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);
This program is shorter, clearer, safer, and comparable in speed to the original version. There is no unsafe cast; no need to label the output manually because the map keys are enums that know how to translate themselves to printable strings; and no possibility for error in computing array indices. The reason that EnumMap is comparable in speed to an ordinal-indexed array is that EnumMap uses such an array internally, but it hides this implementation detail from the programmer, combining the richness and type safety of a Map with the speed of an array. Note that the EnumMap constructor takes the Class object of the key type: this is a bounded type token, which provides runtime generic type information (Item 33).
這個(gè)程序比原來(lái)的版本更短磅摹,更清晰滋迈,更安全,速度也差不多户誓。沒(méi)有不安全的轉(zhuǎn)換饼灿;不需要手動(dòng)標(biāo)記輸出,因?yàn)?Map 的鍵是能轉(zhuǎn)換為可打印字符串的枚舉帝美;在計(jì)算數(shù)組索引時(shí)不可能出錯(cuò)碍彭。EnumMap 在速度上與有序索引數(shù)組相當(dāng)?shù)脑蚴牵珽numMap 在內(nèi)部使用這樣的數(shù)組悼潭,但是它向程序員隱藏了實(shí)現(xiàn)細(xì)節(jié)庇忌,將 Map 的豐富的功能和類(lèi)型安全性與數(shù)組的速度結(jié)合起來(lái)。注意舰褪,EnumMap 構(gòu)造函數(shù)接受鍵類(lèi)型的 Class 對(duì)象:這是一個(gè)有界類(lèi)型標(biāo)記皆疹,它提供運(yùn)行時(shí)泛型類(lèi)型信息(Item-33)。
The previous program can be further shortened by using a stream (Item 45) to manage the map. Here is the simplest stream-based code that largely duplicates the behavior of the previous example:
通過(guò)使用流(Item-45)來(lái)管理映射占拍,可以進(jìn)一步縮短前面的程序略就。下面是基于流的最簡(jiǎn)單的代碼,它在很大程度上復(fù)制了前一個(gè)示例的行為:
// Naive stream-based approach - unlikely to produce an EnumMap!
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));
譯注:以上代碼需要引入 java.util.stream.Collectors.groupingBy
晃酒,輸出結(jié)果如下:
{BIENNIAL=[B, D], ANNUAL=[A], PERENNIAL=[C, E]}
The problem with this code is that it chooses its own map implementation, and in practice it won’t be an EnumMap, so it won’t match the space and time performance of the version with the explicit EnumMap. To rectify this problem, use the three-parameter form of Collectors.groupingBy, which allows the caller to specify the map implementation using the mapFactory parameter:
這段代碼的問(wèn)題在于它選擇了自己的 Map 實(shí)現(xiàn)残制,而實(shí)際上它不是 EnumMap,所以它的空間和時(shí)間性能與顯式 EnumMap 不匹配。要糾正這個(gè)問(wèn)題,可以使用 Collectors.groupingBy
的三參數(shù)形式胯陋,它允許調(diào)用者使用 mapFactory 參數(shù)指定 Map 實(shí)現(xiàn):
// 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()))
);
譯注:以上代碼需要引入 java.util.stream.Collectors.toSet
This optimization would not be worth doing in a toy program like this one but could be critical in a program that made heavy use of the map.
這種優(yōu)化在示例程序中不值得去做赘来,但在大量使用 Map 的程序中可能非常重要。
The behavior of the stream-based versions differs slightly from that of the EmumMap version. The EnumMap version always makes a nested map for each plant lifecycle, while the stream-based versions only make a nested map if the garden contains one or more plants with that lifecycle. So, for example, if the garden contains annuals and perennials but no biennials, the size of plantsByLifeCycle will be three in the EnumMap version and two in both of the stream-based versions.
基于流的版本的行為與 EmumMap 版本略有不同。EnumMap 版本總是為每個(gè)植物生命周期生成一個(gè)嵌套 Map,而基于流的版本只在花園包含具有該生命周期的一個(gè)或多個(gè)植物時(shí)才生成嵌套 Map。例如折汞,如果花園包含一年生和多年生植物,但沒(méi)有兩年生植物盖腿,plantsByLifeCycle 的大小在 EnumMap 版本中為 3爽待,在基于流的版本中為 2。
You may see an array of arrays indexed (twice!) by ordinals used to represent a mapping from two enum values. For example, this program uses such an array to map two phases to a phase transition (liquid to solid is freezing, liquid to gas is boiling, and so forth):
你可能會(huì)看到被序數(shù)索引(兩次t娓)的數(shù)組鸟款,序數(shù)用于表示兩個(gè)枚舉值的映射。例如茂卦,這個(gè)程序使用這樣的一個(gè)數(shù)組來(lái)映射兩個(gè)狀態(tài)到一個(gè)狀態(tài)的轉(zhuǎn)換過(guò)程(液體到固體是凍結(jié)的何什,液體到氣體是沸騰的,等等):
// 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()];
}
}
}
譯注:固體等龙、液體处渣、氣體三態(tài)伶贰,對(duì)應(yīng)的三組變化:融化 MELT,凍結(jié) FREEZE(固態(tài)與液態(tài))罐栈;沸騰 BOIL黍衙,凝固 CONDENSE(液態(tài)與氣態(tài));升華 SUBLIME荠诬,凝華 DEPOSIT(固態(tài)與氣態(tài))们豌。
This program works and may even appear elegant, but appearances can be deceiving. Like the simpler garden example shown earlier, the compiler has no way of knowing the relationship between ordinals and array indices. If you make a mistake in the transition table or forget to update it when you modify the Phase or Phase.Transition enum type, your program will fail at runtime. The failure may be an ArrayIndexOutOfBoundsException, a NullPointerException, or (worse) silent erroneous behavior. And the size of the table is quadratic in the number of phases, even if the number of non-null entries is smaller.
這個(gè)程序可以工作,甚至可能看起來(lái)很優(yōu)雅浅妆,但外表可能具有欺騙性。就像前面展示的更簡(jiǎn)單的 garden 示例一樣障癌,編譯器無(wú)法知道序數(shù)和數(shù)組索引之間的關(guān)系凌外。如果你在轉(zhuǎn)換表中出錯(cuò),或者在修改 Phase 或 Phase.Transition
枚舉類(lèi)型時(shí)忘記更新涛浙,你的程序?qū)⒃谶\(yùn)行時(shí)失敗康辑。失敗可能是拋出 ArrayIndexOutOfBoundsException、NullPointerException 或(更糟糕的)靜默錯(cuò)誤行為轿亮。并且即使非空項(xiàng)的數(shù)目更小疮薇,該表的大小也為狀態(tài)數(shù)量的二次方。
Again, you can do much better with EnumMap. Because each phase transition is indexed by a pair of phase enums, you are best off representing the relationship as a map from one enum (the “from” phase) to a map from the second enum (the “to” phase) to the result (the phase transition). The two phases associated with a phase transition are best captured by associating them with the phase transition enum, which can then be used to initialize the nested EnumMap:
同樣我注,使用 EnumMap 可以做得更好按咒。因?yàn)槊總€(gè)階段轉(zhuǎn)換都由一對(duì)階段枚舉索引,所以最好將這個(gè)關(guān)系用 Map 表示但骨,從一個(gè)枚舉(起始階段)到第二個(gè)枚舉(結(jié)束階段)到結(jié)果(轉(zhuǎn)換階段)励七。與階段轉(zhuǎn)換相關(guān)聯(lián)的兩個(gè)階段最容易捕捉到的是將它們與階段過(guò)渡的 enum 聯(lián)系起來(lái),這樣就可以用來(lái)初始化嵌套的 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 = to;
}
// Initialize the phase transition map
private static final Map<Phase_new, Map<Phase_new, Transition>> m = Stream.of(values())
.collect(groupingBy(
t -> t.from,
() -> new EnumMap<>(Phase_new.class),
toMap(t -> t.to, t -> t, (x, y) -> y, () -> new EnumMap<>(Phase_new.class))
)
);
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
The code to initialize the phase transition map is a bit complicated. The type of the map is Map<Phase, Map<Phase, Transition>>
, which means “map from (source) phase to map from (destination) phase to transition.” This map-of-maps is initialized using a cascaded sequence of two collectors. The first collector groups the transitions by source phase, and the second creates an EnumMap with mappings from destination phase to transition. The merge function in the second collector ((x, y) -> y)) is unused; it is required only because we need to specify a map factory in order to get an EnumMap, and Collectors provides telescoping factories. The previous edition of this book used explicit iteration to initialize the phase transition map. The code was more verbose but arguably easier to understand.
初始化階段變化 Map 的代碼有點(diǎn)復(fù)雜奔缠。Map 的類(lèi)型是 Map<Phase, Map<Phase, Transition>>
掠抬,這意味著「從(源)階段 Map 到(目標(biāo))階段 Map 的轉(zhuǎn)換過(guò)程」。這個(gè) Map 嵌套是使用兩個(gè)收集器的級(jí)聯(lián)序列初始化的校哎。第一個(gè)收集器按源階段對(duì)轉(zhuǎn)換進(jìn)行分組两波,第二個(gè)收集器使用從目標(biāo)階段到轉(zhuǎn)換的映射創(chuàng)建一個(gè) EnumMap。第二個(gè)收集器 ((x, y) -> y) 中的 merge 函數(shù)未使用闷哆;之所以需要它腰奋,只是因?yàn)槲覀冃枰付ㄒ粋€(gè) Map 工廠來(lái)獲得 EnumMap,而 Collector 提供了伸縮工廠抱怔。本書(shū)的上一個(gè)版本使用顯式迭代來(lái)初始化階段轉(zhuǎn)換映射氛堕。代碼更冗長(zhǎng),但也更容易理解野蝇。
譯注:第二版中的實(shí)現(xiàn)代碼如下:
// Initialize the phase transition map
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 trans : Transition.values() )
m.get(trans. src).put(trans.dst, trans) ;
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
Now suppose you want to add a new phase to the system: plasma, or ionized gas. There are only two transitions associated with this phase: ionization, which takes a gas to a plasma; and deionization, which takes a plasma to a gas. To update the array-based program, you would have to add one new constant to Phase and two to Phase.Transition, and replace the original nine-element array of arrays with a new sixteen-element version. If you add too many or too few elements to the array or place an element out of order, you are out of luck: the program will compile, but it will fail at runtime. To update the EnumMap-based version, all you have to do is add PLASMA to the list of phases, and IONIZE(GAS, PLASMA) and DEIONIZE(PLASMA, GAS) to the list of phase transitions:
現(xiàn)在假設(shè)你想向系統(tǒng)中加入一種新階段:等離子體讼稚,或電離氣體括儒。這個(gè)階段只有兩個(gè)變化:電離,它把氣體轉(zhuǎn)為等離子體锐想;去離子作用帮寻,把等離子體變成氣體。假設(shè)要更新基于數(shù)組版本的程序赠摇,必須向 Phase 添加一個(gè)新常量固逗,向 Phase.Transition
添加兩個(gè)新常量,并用一個(gè)新的 16 個(gè)元素版本替換原來(lái)的數(shù)組中的 9 個(gè)元素?cái)?shù)組藕帜。如果你向數(shù)組中添加了太多或太少的元素烫罩,或者打亂了元素的順序,那么你就麻煩了:程序?qū)⒕幾g洽故,但在運(yùn)行時(shí)將失敗贝攒。相比之下,要更新基于 EnumMap 的版本时甚,只需將 PLASMA 添加到 Phase 列表中隘弊,將 IONIZE(GAS, PLASMA)
和 DEIONIZE(PLASMA, GAS)
添加到 Phase.Transition
中:
// 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
}
}
The program takes care of everything else and leaves you virtually no opportunity for error. Internally, the map of maps is implemented with an array of arrays, so you pay little in space or time cost for the added clarity, safety, and ease of maintenance.
這個(gè)程序會(huì)處理所有其他事情,實(shí)際上不會(huì)給你留下任何出錯(cuò)的機(jī)會(huì)荒适。在內(nèi)部梨熙,Map 的映射是用一個(gè)數(shù)組來(lái)實(shí)現(xiàn)的,因此你只需花費(fèi)很少的空間或時(shí)間成本就可以獲得更好的清晰度刀诬、安全性并易于維護(hù)咽扇。
In the interest of brevity, the above examples use null to indicate the absence of a state change (wherein to and from are identical). This is not good practice and is likely to result in a NullPointerException at runtime. Designing a clean, elegant solution to this problem is surprisingly tricky, and the resulting programs are sufficiently long that they would detract from the primary material in this item.
為了簡(jiǎn)潔起見(jiàn),最初的示例使用 null 表示沒(méi)有狀態(tài)更改(其中 to 和 from 是相同的)陕壹。這不是一個(gè)好的方式肌割,可能會(huì)在運(yùn)行時(shí)導(dǎo)致 NullPointerException。針對(duì)這個(gè)問(wèn)題設(shè)計(jì)一個(gè)干凈帐要、優(yōu)雅的解決方案是非常棘手的把敞,并且生成的程序冗長(zhǎng),以至于它們會(huì)偏離條目中的主要內(nèi)容榨惠。
In summary, it is rarely appropriate to use ordinals to index into arrays: use EnumMap instead. If the relationship you are representing is multidimensional, use EnumMap<..., EnumMap<...>>
. This is a special case of the general principle that application programmers should rarely, if ever, use Enum.ordinal (Item 35).
總之奋早,用普通的序數(shù)索引數(shù)組是非常不合適的:應(yīng)使用 EnumMap 代替。 如果所表示的關(guān)系是多維的赠橙,則使用 EnumMap<..., EnumMap<...>>
耽装。這是一種特殊的基本原則,程序員很少(即使有的話)使用 Enum.ordinal
(Item-35)期揪。
Back to contents of the chapter(返回章節(jié)目錄)
- Previous Item(上一條目):Item 36: Use EnumSet instead of bit fields(用 EnumSet 替代位字段)
- Next Item(下一條目):Item 38: Emulate extensible enums with interfaces(使用接口模擬可擴(kuò)展枚舉)