第六章、枚舉和注解

Java 1.5發行版本新增了兩個引用類型家族:枚舉類型(Enumerate類)和注解類型(Annotation接口)。

第三十條、用enum代替int常量

  1. 枚舉類型是指由一組固定的常量組成合法值的類型。替代之前的具名的int常量。
    public static final int APPLE_FUJI = 0;

    這種方式稱作int枚舉模式,存在諸多的不足:
    程序十分脆弱,因為int枚舉是編譯時常量,被編譯到使用它們的客戶端中。如果與枚舉常量關聯的int發生了變化,客戶端就必須重新編譯,否則,程序行為會變得不確定性。(還有個變體是String枚舉模式)

  2. java的枚舉類型是功能十分齊全的類,本質上是int值。
    基本想法是:通過公有的靜態final域為每個枚舉常量導出實例的類。枚舉類型是實例受控的,它們是單例的泛型化,本質上是單元素的枚舉

    枚舉提供了編譯時的類型安全,如果聲明一個參數的類型為Apple,就可以保證,被傳到該參數上的任何非null的對象引用一定屬于三個有效的Apple值之一。試圖傳遞類型錯誤的值時,會導致編譯時錯誤。
    public enum Apple{FUJI,PIPPIN,GERNNY_SMITH}
    包含同名變量的多個枚舉類型可以在一個系統中和平共處,因為每個類型都有自己的命名空間。

  3. 除了彌補int枚舉常量的不足,枚舉類型還允許添加任意的方法和域(近似于類),并實現任意的接口。它們提供了所有Object方法的高級實現,實現了Comparable和Serializable接口。我們為啥要將方法或者域加入到枚舉類型中?將數據與它的常量關聯起來。可以用任何適當的方法來增強枚舉類型。

     /**
      * Created by laneruan on 2017/7/10.
      * 一個枚舉類型的例子
      * 為了將數據與枚舉常量關聯起來,得聲明實例域,并編寫一個帶有數據并將數據保存在域中的構造器。
      */
     public class EnumPlanet {
         public enum Planet{
             MERCURY(3.302e+23,2.439e6),
             VENUS(4.8693+24,6.052e6),
             EARTH(5.975e+24,6.378e6),
             MARS(6.419e+23,3.393e6),
             JUPITER(1.899e+27,7.149e7),
             SATURN(5.685e+26,6.027e7),
             URANUS(8.683e+25,2.556e7),
             NEPTUNE(1.024e+26,2.477e7);
             private final double mass;
             private final double radius;
             private final double surfaceGravity;
             private static final double G = 6.67300E-11;
     
             Planet(double mass,double radius){
                 this.mass = mass;
                 this.radius = radius;
                 surfaceGravity = G * mass/(radius * radius);
             }
             public double mass(){return mass;}
             public double radius(){return radius;}
             public double surfaceGravity(){return surfaceGravity;}
     
             public double surfaceWeight(double mass){
                 return mass * surfaceGravity;
             }
         }
         public static void main(String[] args){
             double earthWeight = Double.parseDouble(args[0]);
             double mass = earthWeight/Planet.EARTH.surfaceGravity();
             for (Planet p : Planet.values()){
                 System.out.println("Weight on " + p +" is "+p.surfaceWeight(mass));
             }
         }
     }
    

    與枚舉常量關聯的有些行為,可能只需要用在定義了枚舉的類或者包中,這種行為最好被實現成私有的或者包級私有的方法。
    如果一個枚舉具有普遍適用性,它就應該成為一個頂層類。如果它是被用在一個特定的頂層類中,它就應該成為該頂層類的一個成員類。

  4. 特定于常量的方法實現:將不同的行為與每個枚舉常量關聯起來

     public 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),
     }
    
  5. 什么時候應該使用枚舉類型?
    每當需要一組固定常量的時候。包括:天然的枚舉類型;在編譯的時候就知道其所有可能值的集合。

  6. 總結:與int常量相比,枚舉類型的優勢不言而喻:易讀,更加安全。功能更加強大。許多枚舉都不需要顯式的構造器或者成員,但許多其他枚舉類型則受益于“每個常量與屬性的關聯”以及“提供行為受這個屬性影響的方法”。


第三十一條、用實例域代替序數

許多枚舉天生就與一個單獨的int值相關聯,所有的枚舉都有一個ordinal方法,它返回每個枚舉常量在類型中的數字位置。可以試著從序數中得到關聯的int值。最好避免使用ordinal方法。

永遠不要根據枚舉的序數導出與它想關聯的值,而是要將它保存在一個實例域中:

    public enum Ensemble{
        SOLO(1),DUET(2),TRIO(3),QUARTET(4),QUINTET(5),
        SETET(6),SEPET(7),OCTET(8),NONET(9),DECTET(10);
        private final int numberOfMusicians;
        Ensemble(int size){
            this.numberOfMusicians = size;
        }
        public int numberOfMusicians(){return numberOfMusicians;}
    //   public int numberOfMusicians(){return ordinal()+1;}
    }

第三十二條、用EnumSet代替位域

需要傳遞多組常量集時,java.util包提供了EnumSet類來有效地表示從單個枚舉類型中提取多個值的多個集合,這個類實現了Set接口,提供了豐富的功能。

    class Text{
        public enum Style {BOLD,ITALIC,UNDERLINE,STRIKETHROUGH}
        public void applyStyles(Set<Style> styles){}
    }
    text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));

總結:正是因為枚舉類型要用在集合中,所以沒有理由用位域來表示他。


第三十三條、用EnumMap來代替序數索引

最好不要用序數來索引數組,而要使用EnumMap。

import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 假設有個香草的數組,表示一座花園中的植物,想要按照類型(一年生、兩年生、多年生)
 * 進行組織之后將這些植物列出來。
 * 注意的是:EnumMap采用了鍵類型的Class對象構造器:這是一個有限制的類型令牌,
 * 提供了運行時的泛型信息。
 */
public class EnumMapHerb {
    public enum Type{ANNUAL,PERENNIAL,BIENNIAL}

    private final String name;
    private final Type type;

    EnumMapHerb(String name,Type type){
        this.name = name;
        this.type = type;
    }
    @Override
    public String toString(){
        return name;
    }
    public static void main(String[] args){
        EnumMapHerb[] garden = null;
        Map<Type,Set<EnumMapHerb>> herbByType =
                new EnumMap<Type, Set<EnumMapHerb>>(EnumMapHerb.Type.class);
        for(EnumMapHerb.Type t :EnumMapHerb.Type.values()){
            herbByType.put(t,new HashSet<EnumMapHerb>());
        }
        for(EnumMapHerb h:garden){
            herbByType.get(h.type).add(h);
        }
    }
}

第三十四條、用接口模擬可伸縮的枚舉

  1. 以操作碼opcode為例:它的元素表示在某種機器上的那些操作。有時候,要盡可能地讓API的用戶提供他們自己的操作,這樣可以有效地擴展API提供的操作集。可以利用枚舉類型來實現這種效果:

     public interface Operation{
         double apply(double x,double y);
     }
     public enum BasicOperation implements Operation{
         PLUS("+"){
             public double apply(double x,double y){
                 return x+y;
             }
         },
         MINUS("-"){
             public double apply(double x,double y){
                 return x-y;
             }
         },  
         TIMES("*"){
             public double apply(double x,double y){
                 return x*y;
             }
         },
         DIVIDE("/"){
             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;
         }
     }
    

    雖然枚舉類型BasicOperaion不是可擴展的,但是接口類型是可擴展的,你可以定義另一個枚舉類型,它實現這個接口,并用這個新類型的實例代替基本類型。

     public enum ExtendOperation implements Operation{
         EXP("^"){
             public double apply(double x,double y){
                 return Math.pow(x,y);
             }
         },
         REMAINDER("%"){
             public double apply(double x,double y){
                 return x % y;
             }
         };
         private final String symbol;
         ExtendOperation(String symbol){
             this.symbol =  symbol;
         }
         @Override
         public String toString(){
             return symbol;
         }
     }
    

    以下是該類型的使用:

     public static void main(String[] args){
         double x = Double.parseDouble(args[0]);
         double y = Double.parseDouble(args[1]);
         test(ExtendOperation.class,x,y);
     }
     //很復雜的聲明確保了對象既表示枚舉又表示Operation的子類型
     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));
         }
     }
    

    第二種使用方法是使用Collection<? extends Operation>,這是個有限制的通配符類型:

     public static void main(String[] args){
         double x = Double.parseDouble(args[0]);
         double y = Double.parseDouble(args[1]);
         test(Arrays.asList(ExtendOperation.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));
         }
     }
    

這樣得到的代碼不復雜且更靈活:允許操作者將多個實現類型的操作合并到一起。

  1. 總結:雖然無法編寫可擴展的枚舉類型,但可以通過編寫接口以及實現該接口的基礎枚舉類型,對它進行模擬。

第三十五條、注解優先于命名模式(Naming Pattern)

  1. Java 1.5之前一般使用命名模式表明有些程序元素需要通過某種工具或者框架進行特殊處理。這種模式缺點很明顯:文字拼寫錯誤很容易發生且難以發覺;無法確保它們只用于相應的程序元素上;沒有提供將參數值與程序元素相關聯起來的好方法

  2. 注解很好地解決了這些問題:
    假設想要定義一個注解類型RunTests來指定簡單的測試,它們自動運行,并在拋出異常時失敗:

     import java.lang.annotation.*;
     @Retention(RetentionPolicy.RUNTIME) //表明注解應在運行時保留,元注解
     @Target(ElementType.METHOD)  //表明只有在方法中聲明才是合法的,元注解
     public @interface RunTests {
     }
    
  3. 現實應用中的Test注解,稱作標記注解(marker annotation)。

  4. 總結:大多數程序員不需要定義注解類型,所有的程序員都應該使用Java平臺所提供的預定義的注解類型,還要考慮使用IDE或者靜態分析工具所提供的任何注解。

第三十六條、堅持使用@Override注解

  1. Override注解只能在方法聲明時使用,表示被注解的方法聲明覆蓋了超類中的一個聲明。堅持使用這一注解,可以防止一大類的非法錯誤。

  2. 現代的IDE提供了堅持使用@Override的另一種理由:IDE具有自動檢查功能,稱作代碼檢驗(code inspection),當有一個方法沒有@Override注解卻覆蓋了超類方法時,IDE會產生一條警告提醒你警惕無意識的覆蓋。

  3. 總結:如果你想要的每個方法聲明中使用Override注解來覆蓋超類聲明,編譯器就可以替你防止大量的錯誤。但有一個例外:在具體的類中,不必標注你確信覆蓋了抽象方法聲明的方法。

第三十七條、用標記接口定義類型

  1. 標記接口(Marker Interface)是沒有包含方法聲明的接口,只是指明(或者標明)一個類實現了具有某種屬性的接口。例:Serializable接口,通過實現這個接口,類表明它的實例可以被寫到ObjectOutputStream(即“被序列化”)

  2. 標記注解和標記接口:

    • 標記接口的優點:
      • 標記接口定義的類型是由被標記類的實例實現的;標記注解則沒有定義這樣的類型。這個類型允許你在編譯時捕捉在使用標記注解的情況要到運行時才能捕捉到的錯誤。
      • 他們可以更加精確地進行鎖定。
    • 標記注解的優點:
      • 它可以通過默認的方式添加一個或者多個注解類型元素,給已被使用的注解類型添加更多的信息。隨著時間的遷移,簡單的標記注解可以演變成更加豐富的注解類型,這在標記接口中是不可能的;
      • 它們是更大的注解機制的一部分。因此,標記注解在那些支持注解作為編程元素之一的框架中同樣具有一致性。
  3. 何時使用標記注解和標記接口?

    如果標記是應用到任何程序元素而不是類或者接口,就必須使用注解,因為只有類和接可以用來實現或者擴展接口。如果標記只應用在類和接口,思考下:我要編寫一個或者多個只接受有這種標記的方法嗎?如果是這樣,優先使用標記接口。如果不是,再思考下:是否要永遠限制這個標記只用于特殊接口的元素?如果是,則優先使用標記接口。 最后選擇使用標記注解。

  4. 總結:標記接口和標記注解都有用處,如何選擇見上。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容

  • 枚舉類型是指由一組固定的常量組成合法值的類型,例如一年中的季節,太陽系中的行星或者一副牌中的花色。在編程語言...
    小小輝_710a閱讀 1,448評論 0 0
  • 經典重讀——亞馬遜鏈接 筆記鏈接 導圖: 筆記文本: Effective Java1 第2章 創建和銷毀對象1.1...
    8c3c932b5ffd閱讀 1,440評論 0 1
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,804評論 18 139
  • 對象的創建與銷毀 Item 1: 使用static工廠方法,而不是構造函數創建對象:僅僅是創建對象的方法,并非Fa...
    孫小磊閱讀 2,015評論 0 3
  • 人生最快樂的事不是高官厚祿的擔驚受怕,也不是舒適安逸的碌碌無為,更不是日復一日的枯燥生活。 我認為最快樂的事就是,...
    公子涼閱讀 2,750評論 8 13