枚舉和注解是Java1.5版本中新增的特性,本章討論使用它們時的最佳實踐。
本章內(nèi)容導(dǎo)圖:

1.用enum代替int常量
枚舉類型是指由一組固定的常量組成合法值的類型,如一年中的季節(jié)、太陽系中的行星、一副牌中的花色等。在編程語言沒有引入枚舉之前,表示枚舉類型的常用模式是聲明一組具名的int常量,每個類型成員一個常量:
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
這種方法稱作int枚舉模式,它存在著諸多不足:
1.類型安全性問題
可能會傳遞錯誤的值
2.沒有自己的命名空間
一般只能通過前綴的形式區(qū)分
3.采用int枚舉模式的程序十分脆弱
int枚舉是編譯時常量,被編譯到使用它們的客戶端中,如果枚舉常量值發(fā)生了變化,客戶端必須重新編譯才行。
4.無法提供便利的方法打印信息
int枚舉的打印信息只是數(shù)字
String枚舉模式是int枚舉模式的變體,雖然它可以提供可打印的字符串,但存在性能及書寫時的安全性問題。
Java1.5開始,提供了枚舉類型,它不僅可以避免int枚舉模式和String枚舉模式的缺點(diǎn),還可以提供許多額外的好處:
public enum Apple {
FUJI,
PIPPIN,
GRANNY_SMITH
}
枚舉的好處有:
1.提供編譯時的類型安全
如果聲明一個參數(shù)的類型為枚舉類型Apple,就可以保證,被傳遞到該參數(shù)上的任何非null的對象引用一定屬于三個有效的Apple之一。試圖傳遞類型錯誤的值時,會導(dǎo)致編譯錯誤。
2.每個枚舉類型都有自己的命名空間
枚舉類是獨(dú)立的類型,有自己的命名空間,可以增加或者重新排列枚舉類型中的常量。
3.可提供便利的打印信息
通過toString(),可以將枚舉轉(zhuǎn)換成可打印的字符串。
4.允許添加任意的方法和域,并實現(xiàn)任意的接口
枚舉是一種類型,可以擁有自己的方法和域,并實現(xiàn)接口。
枚舉的缺點(diǎn):
1.裝載和初始化枚舉時會有空間和時間的成本
在枚舉中添加域和方法的動機(jī):
1.想將數(shù)據(jù)與它的常量關(guān)聯(lián)起來
2.添加方法增強(qiáng)枚舉類型功能
如果一個枚舉具有普適性,就應(yīng)該成為一個頂層類;如果它只是被用在一個特定的頂層類中,就應(yīng)該成為該頂層類的一個成員類。
在枚舉類中添加方法時,這些方法是枚舉常量共有的,但有時每個常量都會關(guān)聯(lián)本質(zhì)上完全不同的行為,可以使用特定于常量的方法實現(xiàn)來完成。它的實現(xiàn)過程如下:
1.在枚舉類型中聲明一個抽象的方法
2.在特定常量的類主體中,用具體的方法實現(xiàn)抽象方法
enum Operation {
PLUS {
@Override
double apply(double x, double y) {
return x + y;
}
},
MINUS {
@Override
double apply(double x, double y) {
return x - y;
}
},
TIMES {
@Override
double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override
double apply(double x, double y) {
return x / y;
}
};
abstract double apply(double x, double y);
}
使用枚舉的時機(jī):
每當(dāng)需要一組固定常量的時候。
1.包括“天然的枚舉類型”,如行星、一周的天數(shù)、一年中的季節(jié)等;
2.包括在編譯時就知道其所有可能值的其他集合,如操作代碼、命令行標(biāo)記、菜單的選項等。
枚舉類型中的常量集并不一定要始終保持不變,專門設(shè)計枚舉特性也是考慮到枚舉類型二進(jìn)制兼容演變的需求。
與int常量相比,枚舉類型的優(yōu)勢很多。枚舉更加易讀,也更加安全,功能更加強(qiáng)大。
許多枚舉都不需要顯式的構(gòu)造器或者成員,但如有需求,你可以提供與常量相關(guān)聯(lián)的屬性和方法。還可以使用特定于常量的方法將多種行為與單個方法關(guān)聯(lián)。
如果多個枚舉常量同時共享相同的行為,可考慮策略枚舉。
2.用實例域代替序數(shù)
所有的枚舉都有一個ordinal方法,它返回每個枚舉常量在類型中的數(shù)組位序。
依賴ordinal()返回的枚舉常量序數(shù)會使得代碼極難維護(hù)。因為枚舉常量可能會進(jìn)行重新排序,也可能會添加新的枚舉常量。
永遠(yuǎn)不要根據(jù)枚舉序數(shù)去得到與它關(guān)聯(lián)的值,而是要將它保存在一個實例域中。
//不當(dāng)?shù)氖褂梅绞?public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
//依賴ordinal()返回與枚舉常量關(guān)聯(lián)的值
public int numberOfMusicians() {
return ordinal() + 1;
}
}
//推薦的使用方式
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
Enum規(guī)范中對ordinal()的描述為:大多數(shù)程序員都不需要這個方法,它被設(shè)計成用于像EnumSet、EnumMap這種基于枚舉的通用數(shù)據(jù)結(jié)構(gòu)的。除非你在編寫這種數(shù)據(jù)結(jié)構(gòu),否則最好完全避免使用ordinal方法。
3.用EnumSet代替位域
如果一個枚舉類型的元素主要用在集合中,可能會使用int枚舉模式:
public class Text {
public static final int STYLE_BOLD = 1 << 0; //1
public static final int STYLE_ITALIC = 1 << 1; //2
public static final int STYLE_UNDERLINE = 1 << 2; //4
public static final int STYLE_STRIKETHROUGH = 1 << 3; //8
public void applyStyles(int styles) {
...
}
}
這種表示法讓你用or位運(yùn)算符將幾個常量合并到一個集合中,這個集合稱作位域:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
位域表示法也允許利用位操作,執(zhí)行像交集、并集這樣的集合操作。但位域具有int枚舉常量所有的缺點(diǎn),甚至更多。位域以數(shù)字形式打印時,翻譯位域比翻譯int枚舉常量要困難的多,遍歷位域表示的所有元素也相當(dāng)不容易。
Set是一種集合,只能向其中添加不重復(fù)的對象,enum也要求其成員都是唯一的,看起來也具有集合的行為,但不能從enum中刪除/添加元素。Java1.5引入了EnumSet替代傳統(tǒng)的基于int枚舉類型的位域集合,它表示從單個枚舉類型中提取多個枚舉值的集合。
EnumSet是與enum類型一起使用的專用Set類型,EnumSet中的所有元素都必須來自同一個enum。
使用EnumSet代替位域后的代碼更加簡短、更加清楚、更加安全:
public class Text {
public enum Style {
BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
}
public void applyStyles(Set<Style> styles) {
...
}
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
EnumSet設(shè)計時充分考慮了性能因素,它內(nèi)部將一個long值作為比特向量,且其of()被重載了很多次,不但為可變數(shù)量的參數(shù)進(jìn)行了重載,而且為接收2-5個顯式的參數(shù)情況都進(jìn)行了重載,這也從側(cè)面表現(xiàn)了EnumSet對性能的關(guān)注。

只使用可變參數(shù)已經(jīng)可以解決整個問題了,但是對比顯式參數(shù),會有一點(diǎn)性能損失。因為可變參數(shù)機(jī)制是通過先創(chuàng)建一個數(shù)組,然后將參數(shù)值傳到數(shù)組中,最后將數(shù)組傳遞給方法的。
4.用EnumMap代替序數(shù)索引
Enum的ordinal()返回枚舉常量的序數(shù)。
有時候,會見到利用枚舉常量的序數(shù)作為數(shù)組下標(biāo)來索引數(shù)組的代碼,對應(yīng)映射關(guān)系如下圖所示:

這種方法的確可行,但是隱藏著很多問題:
1.數(shù)組不能與泛型兼容,使其使用受限
2.數(shù)組不知道它的索引代表著什么,需要手工標(biāo)注
3.錯誤的索引值會引發(fā)數(shù)組越界異常
Java1.5版本引入了EnumMap類型,它是一種特殊的Map,它要求其中的key必須來自一個enum,使用enum實例作為鍵在EnumMap中進(jìn)行各種操作。EnumMap在運(yùn)行速度方面可以與數(shù)組相媲美,它在內(nèi)部實現(xiàn)中使用了數(shù)組,但是它對程序員隱藏了實現(xiàn)細(xì)節(jié),它具有Map的豐富功能、類型安全,以及數(shù)組的快速訪問。映射關(guān)系如下圖:

最好不要用序數(shù)來索引數(shù)組,而要使用EnumMap。
應(yīng)用程序的程序員在一般情況下都不使用Enum.ordinal()。
5.用接口模擬可伸縮的枚舉
枚舉類型不可擴(kuò)展,但有時又需要枚舉類型具備可伸縮的特性,一種好的方法就是利用接口:
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
@Override
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
public enum ExtendedOperation implements Operation {
EXP("^") {
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
@Override
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
只要API是被寫成采用接口類型(Operation)而非實現(xiàn)(BasicOperation),那么在可以使用基礎(chǔ)操作的任何地方,都可以使用新的操作。
//方式一
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(Class<T> opSet, double x, double y) {
for (Operation op : opSet.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
//方式二
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
雖然無法編寫可擴(kuò)展的枚舉類型,卻可以通過編寫接口以及實現(xiàn)該接口的基礎(chǔ)枚舉類型,對它進(jìn)行模擬。這樣允許客戶端編寫自己的枚舉來實現(xiàn)接口。
如果API是根據(jù)接口編寫的,那么在可以使用基礎(chǔ)枚舉類型的任何地方,也都可以使用這些枚舉。
6.注解優(yōu)先于命名模式
Java1.5版本之前,一般使用命名模式表明有些程序元素需要通過某種工具或者框架進(jìn)行特殊處理。例如,JUnit4之前原本要求測試方法要以test作為開頭。這種方法可行,但有幾個很嚴(yán)重的缺點(diǎn)。
命名模式的缺點(diǎn)有:
1.文字拼寫錯誤會導(dǎo)致失敗,且沒有任何提示。
2.無法確保它們只用于相應(yīng)的程序元素上。
??如將某個類稱作testSafetyMechanisms,希望JUnit可以自動地測試它的所有方法,而不管類中的方法名字是什么。雖然JUnit不會出錯,但也不會執(zhí)行測試。
3.沒有提供將參數(shù)值與程序元素關(guān)聯(lián)起來的好方法。
注解很好地解決命名模式的所有問題,因此,Java1.5版本后,JUnit4使用注解代替命名模式,重新實現(xiàn)了整個測試框架,使之更加強(qiáng)大、易用。
7.堅持使用Override注解
Override注解只能用在方法聲明中,它表示被注解的方法聲明覆蓋(重寫)了超類型中的一個方法聲明。堅持使用這個注解,可以防止一大類的非法錯誤。這類錯誤基本上都是由于不小心而造成的,使用Override注解后,編譯器會做自動檢查,可以避免這類無意識的錯誤。
8.用標(biāo)記接口定義類型
標(biāo)記接口是沒有包含方法聲明的接口,它只是標(biāo)明一個類實現(xiàn)了具有某種屬性的接口。例如,通過實現(xiàn)Serializable接口,表明類的實例可以被序列化。
標(biāo)記注解:一種被用來“標(biāo)注”程序元素的注解。
標(biāo)記接口的優(yōu)點(diǎn):
1.標(biāo)記接口定義的類型是由被標(biāo)記類的實例實現(xiàn)的,允許在編譯時發(fā)現(xiàn)標(biāo)記接口的使用錯誤。
2.標(biāo)記接口可以被更加精確地進(jìn)行鎖定,它可以用來標(biāo)記某類特殊接口的實現(xiàn)。
標(biāo)記注解的優(yōu)點(diǎn):
1.它可以通過默認(rèn)的方式添加一個或者多個注解類型元素,給已被使用的注解類型添加更多信息。
2.它是更大的注解機(jī)制的一部分,在那些支持注解作為編程元素的框架中具有一致性。
標(biāo)記接口和標(biāo)記注解的使用選擇:
如果標(biāo)記是用到程序元素而不是類或接口,要使用注解;
如果標(biāo)記只應(yīng)用給類和接口,就該優(yōu)先使用接口。
標(biāo)記接口和標(biāo)記注解各有用處。
如果要定義一個任何新方法都不會與之關(guān)聯(lián)的類型,標(biāo)記接口就是最好的選擇。
如果要標(biāo)記程序元素而非類和接口,考慮到未來可能要給標(biāo)記添加更多信息,或者標(biāo)記要適合于已經(jīng)廣泛使用了注解類型的框架,標(biāo)記注解就是正確的選擇。