Effective Java-枚舉和注解

枚舉和注解是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)系如下圖所示:

Enum序數(shù)作為數(shù)組索引

這種方法的確可行,但是隱藏著很多問題:
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)系如下圖:

EnumMap映射

最好不要用序數(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)記注解就是正確的選擇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祖很,一起剝皮案震驚了整個(gè)濱河市笛丙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌突琳,老刑警劉巖若债,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拆融,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)啊终,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門镜豹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蓝牲,你說我怎么就攤上這事趟脂。” “怎么了例衍?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵昔期,是天一觀的道長。 經(jīng)常有香客問我佛玄,道長硼一,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任梦抢,我火速辦了婚禮般贼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己哼蛆,他們只是感情好蕊梧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腮介,像睡著了一般肥矢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叠洗,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天甘改,我揣著相機(jī)與錄音,去河邊找鬼惕味。 笑死楼誓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的名挥。 我是一名探鬼主播疟羹,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼禀倔!你這毒婦竟也來了榄融?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤救湖,失蹤者是張志新(化名)和其女友劉穎愧杯,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鞋既,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡力九,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了邑闺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跌前。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖陡舅,靈堂內(nèi)的尸體忽然破棺而出抵乓,到底是詐尸還是另有隱情,我是刑警寧澤靶衍,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布灾炭,位于F島的核電站,受9級(jí)特大地震影響颅眶,放射性物質(zhì)發(fā)生泄漏蜈出。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一帚呼、第九天 我趴在偏房一處隱蔽的房頂上張望掏缎。 院中可真熱鬧皱蹦,春花似錦、人聲如沸眷蜈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酌儒。三九已至辜妓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忌怎,已是汗流浹背籍滴。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榴啸,地道東北人孽惰。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像鸥印,于是被迫代替她去往敵國和親勋功。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Chapter 6 Enums and Annotations 枚舉和注解 JAVA supports two s...
    LaMole閱讀 812評(píng)論 0 2
  • 30、用enum代替int常量 枚舉類型是指由一組固定的常量組成合法值的類型潜的。在java沒有引入枚舉類型前骚揍,表示枚...
    Alent閱讀 758評(píng)論 1 5
  • Java 1.5發(fā)行版本新增了兩個(gè)引用類型家族:枚舉類型(Enumerate類)和注解類型(Annotation接...
    Timorous閱讀 408評(píng)論 0 0
  • 第6章 枚舉和注解 第30條:用 enum 代替 int 常量 在沒有 enum 之前表示枚舉類型的常用模式時(shí)聲...
    bruvir閱讀 271評(píng)論 0 0
  • 近期各大新聞都在說信不,萬達(dá)高管跳樓自殺的事情,我身邊也有房地產(chǎn)同僚亡呵,因?yàn)榘┌Y與近期去世浑塞,享年只有34歲。越來越多30...
    樹_ce9c閱讀 181評(píng)論 0 0