Effective Java-枚舉和注解

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

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

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

EnumMap映射

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容