枚舉和注解是Java1.5版本中新增的特性该面,本章討論使用它們時(shí)的最佳實(shí)踐。
本章內(nèi)容導(dǎo)圖:
1.用enum代替int常量
枚舉類型是指由一組固定的常量組成合法值的類型,如一年中的季節(jié)筷弦、太陽系中的行星、一副牌中的花色等抑诸。在編程語言沒有引入枚舉之前烂琴,表示枚舉類型的常用模式是聲明一組具名的int常量,每個(gè)類型成員一個(gè)常量:
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
這種方法稱作int枚舉模式蜕乡,它存在著諸多不足:
1.類型安全性問題
可能會(huì)傳遞錯(cuò)誤的值
2.沒有自己的命名空間
一般只能通過前綴的形式區(qū)分
3.采用int枚舉模式的程序十分脆弱
int枚舉是編譯時(shí)常量奸绷,被編譯到使用它們的客戶端中,如果枚舉常量值發(fā)生了變化层玲,客戶端必須重新編譯才行号醉。
4.無法提供便利的方法打印信息
int枚舉的打印信息只是數(shù)字
String枚舉模式是int枚舉模式的變體,雖然它可以提供可打印的字符串辛块,但存在性能及書寫時(shí)的安全性問題畔派。
Java1.5開始,提供了枚舉類型润绵,它不僅可以避免int枚舉模式和String枚舉模式的缺點(diǎn)线椰,還可以提供許多額外的好處:
public enum Apple {
FUJI,
PIPPIN,
GRANNY_SMITH
}
枚舉的好處有:
1.提供編譯時(shí)的類型安全
如果聲明一個(gè)參數(shù)的類型為枚舉類型Apple,就可以保證尘盼,被傳遞到該參數(shù)上的任何非null的對(duì)象引用一定屬于三個(gè)有效的Apple之一憨愉。試圖傳遞類型錯(cuò)誤的值時(shí)烦绳,會(huì)導(dǎo)致編譯錯(cuò)誤。
2.每個(gè)枚舉類型都有自己的命名空間
枚舉類是獨(dú)立的類型配紫,有自己的命名空間径密,可以增加或者重新排列枚舉類型中的常量。
3.可提供便利的打印信息
通過toString()笨蚁,可以將枚舉轉(zhuǎn)換成可打印的字符串睹晒。
4.允許添加任意的方法和域趟庄,并實(shí)現(xiàn)任意的接口
枚舉是一種類型括细,可以擁有自己的方法和域,并實(shí)現(xiàn)接口戚啥。
枚舉的缺點(diǎn):
1.裝載和初始化枚舉時(shí)會(huì)有空間和時(shí)間的成本
在枚舉中添加域和方法的動(dòng)機(jī):
1.想將數(shù)據(jù)與它的常量關(guān)聯(lián)起來
2.添加方法增強(qiáng)枚舉類型功能
如果一個(gè)枚舉具有普適性奋单,就應(yīng)該成為一個(gè)頂層類;如果它只是被用在一個(gè)特定的頂層類中猫十,就應(yīng)該成為該頂層類的一個(gè)成員類览濒。
在枚舉類中添加方法時(shí),這些方法是枚舉常量共有的拖云,但有時(shí)每個(gè)常量都會(huì)關(guān)聯(lián)本質(zhì)上完全不同的行為贷笛,可以使用特定于常量的方法實(shí)現(xiàn)來完成。它的實(shí)現(xiàn)過程如下:
1.在枚舉類型中聲明一個(gè)抽象的方法
2.在特定常量的類主體中宙项,用具體的方法實(shí)現(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);
}
使用枚舉的時(shí)機(jī):
每當(dāng)需要一組固定常量的時(shí)候乏苦。
1.包括“天然的枚舉類型”,如行星尤筐、一周的天數(shù)汇荐、一年中的季節(jié)等;
2.包括在編譯時(shí)就知道其所有可能值的其他集合盆繁,如操作代碼掀淘、命令行標(biāo)記、菜單的選項(xiàng)等油昂。
枚舉類型中的常量集并不一定要始終保持不變革娄,專門設(shè)計(jì)枚舉特性也是考慮到枚舉類型二進(jìn)制兼容演變的需求。
與int常量相比冕碟,枚舉類型的優(yōu)勢很多拦惋。枚舉更加易讀,也更加安全鸣哀,功能更加強(qiáng)大架忌。
許多枚舉都不需要顯式的構(gòu)造器或者成員,但如有需求我衬,你可以提供與常量相關(guān)聯(lián)的屬性和方法叹放。還可以使用特定于常量的方法將多種行為與單個(gè)方法關(guān)聯(lián)饰恕。
如果多個(gè)枚舉常量同時(shí)共享相同的行為,可考慮策略枚舉井仰。
2.用實(shí)例域代替序數(shù)
所有的枚舉都有一個(gè)ordinal方法埋嵌,它返回每個(gè)枚舉常量在類型中的數(shù)組位序。
依賴ordinal()
返回的枚舉常量序數(shù)會(huì)使得代碼極難維護(hù)俱恶。因?yàn)槊杜e常量可能會(huì)進(jìn)行重新排序雹嗦,也可能會(huì)添加新的枚舉常量。
永遠(yuǎn)不要根據(jù)枚舉序數(shù)去得到與它關(guān)聯(lián)的值合是,而是要將它保存在一個(gè)實(shí)例域中了罪。
//不當(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ī)范中對(duì)ordinal()的描述為:大多數(shù)程序員都不需要這個(gè)方法,它被設(shè)計(jì)成用于像EnumSet聪全、EnumMap這種基于枚舉的通用數(shù)據(jù)結(jié)構(gòu)的泊藕。除非你在編寫這種數(shù)據(jù)結(jié)構(gòu),否則最好完全避免使用ordinal方法难礼。
3.用EnumSet代替位域
如果一個(gè)枚舉類型的元素主要用在集合中娃圆,可能會(huì)使用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)算符將幾個(gè)常量合并到一個(gè)集合中,這個(gè)集合稱作位域:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
位域表示法也允許利用位操作蛾茉,執(zhí)行像交集讼呢、并集這樣的集合操作。但位域具有int枚舉常量所有的缺點(diǎn)谦炬,甚至更多悦屏。位域以數(shù)字形式打印時(shí),翻譯位域比翻譯int枚舉常量要困難的多吧寺,遍歷位域表示的所有元素也相當(dāng)不容易窜管。
Set是一種集合,只能向其中添加不重復(fù)的對(duì)象稚机,enum也要求其成員都是唯一的幕帆,看起來也具有集合的行為古戴,但不能從enum中刪除/添加元素耀里。Java1.5引入了EnumSet替代傳統(tǒng)的基于int枚舉類型的位域集合痢掠,它表示從單個(gè)枚舉類型中提取多個(gè)枚舉值的集合仙逻。
EnumSet是與enum類型一起使用的專用Set類型竿滨,EnumSet中的所有元素都必須來自同一個(gè)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è)計(jì)時(shí)充分考慮了性能因素仿贬,它內(nèi)部將一個(gè)long值作為比特向量纽竣,且其of()被重載了很多次,不但為可變數(shù)量的參數(shù)進(jìn)行了重載,而且為接收2-5個(gè)顯式的參數(shù)情況都進(jìn)行了重載蜓氨,這也從側(cè)面表現(xiàn)了EnumSet對(duì)性能的關(guān)注聋袋。
只使用可變參數(shù)已經(jīng)可以解決整個(gè)問題了,但是對(duì)比顯式參數(shù)穴吹,會(huì)有一點(diǎn)性能損失幽勒。因?yàn)榭勺儏?shù)機(jī)制是通過先創(chuàng)建一個(gè)數(shù)組,然后將參數(shù)值傳到數(shù)組中港令,最后將數(shù)組傳遞給方法的啥容。
4.用EnumMap代替序數(shù)索引
Enum的ordinal()返回枚舉常量的序數(shù)。
有時(shí)候顷霹,會(huì)見到利用枚舉常量的序數(shù)作為數(shù)組下標(biāo)來索引數(shù)組的代碼咪惠,對(duì)應(yīng)映射關(guān)系如下圖所示:
這種方法的確可行,但是隱藏著很多問題:
1.數(shù)組不能與泛型兼容泼返,使其使用受限
2.數(shù)組不知道它的索引代表著什么硝逢,需要手工標(biāo)注
3.錯(cuò)誤的索引值會(huì)引發(fā)數(shù)組越界異常
Java1.5版本引入了EnumMap類型,它是一種特殊的Map绅喉,它要求其中的key必須來自一個(gè)enum,使用enum實(shí)例作為鍵在EnumMap中進(jìn)行各種操作叫乌。EnumMap在運(yùn)行速度方面可以與數(shù)組相媲美柴罐,它在內(nèi)部實(shí)現(xiàn)中使用了數(shù)組,但是它對(duì)程序員隱藏了實(shí)現(xiàn)細(xì)節(jié)憨奸,它具有Map的豐富功能革屠、類型安全,以及數(shù)組的快速訪問排宰。映射關(guān)系如下圖:
最好不要用序數(shù)來索引數(shù)組似芝,而要使用EnumMap。
應(yīng)用程序的程序員在一般情況下都不使用Enum.ordinal()板甘。
5.用接口模擬可伸縮的枚舉
枚舉類型不可擴(kuò)展党瓮,但有時(shí)又需要枚舉類型具備可伸縮的特性,一種好的方法就是利用接口:
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)而非實(shí)現(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ò)展的枚舉類型在跳,卻可以通過編寫接口以及實(shí)現(xiàn)該接口的基礎(chǔ)枚舉類型枪萄,對(duì)它進(jìn)行模擬。這樣允許客戶端編寫自己的枚舉來實(shí)現(xiàn)接口猫妙。
如果API是根據(jù)接口編寫的瓷翻,那么在可以使用基礎(chǔ)枚舉類型的任何地方,也都可以使用這些枚舉。
6.注解優(yōu)先于命名模式
Java1.5版本之前齐帚,一般使用命名模式表明有些程序元素需要通過某種工具或者框架進(jìn)行特殊處理元践。例如,JUnit4之前原本要求測試方法要以test
作為開頭童谒。這種方法可行单旁,但有幾個(gè)很嚴(yán)重的缺點(diǎn)。
命名模式的缺點(diǎn)有:
1.文字拼寫錯(cuò)誤會(huì)導(dǎo)致失敗饥伊,且沒有任何提示象浑。
2.無法確保它們只用于相應(yīng)的程序元素上。
??如將某個(gè)類稱作testSafetyMechanisms琅豆,希望JUnit可以自動(dòng)地測試它的所有方法愉豺,而不管類中的方法名字是什么。雖然JUnit不會(huì)出錯(cuò)茫因,但也不會(huì)執(zhí)行測試蚪拦。
3.沒有提供將參數(shù)值與程序元素關(guān)聯(lián)起來的好方法。
注解很好地解決命名模式的所有問題冻押,因此驰贷,Java1.5版本后,JUnit4使用注解代替命名模式洛巢,重新實(shí)現(xiàn)了整個(gè)測試框架括袒,使之更加強(qiáng)大、易用稿茉。
7.堅(jiān)持使用Override注解
Override注解只能用在方法聲明中锹锰,它表示被注解的方法聲明覆蓋(重寫)了超類型中的一個(gè)方法聲明。堅(jiān)持使用這個(gè)注解漓库,可以防止一大類的非法錯(cuò)誤恃慧。這類錯(cuò)誤基本上都是由于不小心而造成的,使用Override注解后渺蒿,編譯器會(huì)做自動(dòng)檢查痢士,可以避免這類無意識(shí)的錯(cuò)誤。
8.用標(biāo)記接口定義類型
標(biāo)記接口是沒有包含方法聲明的接口蘸嘶,它只是標(biāo)明一個(gè)類實(shí)現(xiàn)了具有某種屬性的接口良瞧。例如,通過實(shí)現(xiàn)Serializable接口训唱,表明類的實(shí)例可以被序列化褥蚯。
標(biāo)記注解:一種被用來“標(biāo)注”程序元素的注解。
標(biāo)記接口的優(yōu)點(diǎn):
1.標(biāo)記接口定義的類型是由被標(biāo)記類的實(shí)例實(shí)現(xiàn)的况增,允許在編譯時(shí)發(fā)現(xiàn)標(biāo)記接口的使用錯(cuò)誤赞庶。
2.標(biāo)記接口可以被更加精確地進(jìn)行鎖定,它可以用來標(biāo)記某類特殊接口的實(shí)現(xiàn)。
標(biāo)記注解的優(yōu)點(diǎn):
1.它可以通過默認(rèn)的方式添加一個(gè)或者多個(gè)注解類型元素歧强,給已被使用的注解類型添加更多信息澜薄。
2.它是更大的注解機(jī)制的一部分,在那些支持注解作為編程元素的框架中具有一致性摊册。
標(biāo)記接口和標(biāo)記注解的使用選擇:
如果標(biāo)記是用到程序元素而不是類或接口肤京,要使用注解;
如果標(biāo)記只應(yīng)用給類和接口茅特,就該優(yōu)先使用接口忘分。
標(biāo)記接口和標(biāo)記注解各有用處。
如果要定義一個(gè)任何新方法都不會(huì)與之關(guān)聯(lián)的類型白修,標(biāo)記接口就是最好的選擇妒峦。
如果要標(biāo)記程序元素而非類和接口,考慮到未來可能要給標(biāo)記添加更多信息兵睛,或者標(biāo)記要適合于已經(jīng)廣泛使用了注解類型的框架肯骇,標(biāo)記注解就是正確的選擇。