Effective java筆記(五),枚舉和注解

30、用enum代替int常量

枚舉類型是指由一組固定的常量組成合法值的類型。在java沒有引入枚舉類型前,表示枚舉類型的常用方法是聲明一組不同的int常量,每個類型成員一個常量,這種方法稱作int枚舉模式。采用int枚舉模式的程序是十分脆弱的,因為int值是編譯時常量,若與枚舉常量關(guān)聯(lián)的int發(fā)生變化,客戶端就必須重新編譯。

  • java枚舉類型背后的思想:通過公有的靜態(tài)final域為每個枚舉常量導(dǎo)出實例的類。因為沒有可以訪問的構(gòu)造器,枚舉類型是真正的final。客戶端既不能創(chuàng)建枚舉類型的實例,也不能對它進行擴展。枚舉類型是實例受控的,它們是單例的泛型化,本質(zhì)上是單元素的枚舉。枚舉提供了編譯時的類型安全。

  • 包含同名常量的多個枚舉類型可以在一個系統(tǒng)中和平共處,因為每個類型都有自己的命名空間。可以增加或重新排列枚舉類型中的常量,而無需重新編譯客戶端代碼,因為常量值并沒有被編譯到客戶端代碼中。可以調(diào)用toString方法,將枚舉轉(zhuǎn)換成可打印的字符串。

枚舉類型允許添加任意的方法和域,并實現(xiàn)任意的接口。枚舉類型默認(rèn)繼承Enum類(其實現(xiàn)了Comparable、Serializable接口)。為了將數(shù)據(jù)與枚舉常量關(guān)聯(lián)起來,得聲明實例域,并編寫一個將數(shù)據(jù)保存到域中的構(gòu)造器。枚舉天生就是不可變的,所有的域都必須是final的。

例如:


public enum Planet {
    //括號中數(shù)值為傳遞給構(gòu)造器的參數(shù)
    MERCURY(3.302e+23, 2.439e6),
    VENUS(4.869e+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; //質(zhì)量kg
    private final double radius; //半徑
    private final double surfaceGravity; //表面重力,final常量構(gòu)造器中必須初始化

    private static final double G = 6.673E-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 = 175;
        double mass = earthWeight/Planet.EARTH.surfaceGravity();
        for(Planet p : Planet.values()) {
            //java的printf方法中換行用%n, C語言中用\n
            System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass)); 
        }
    }
}

上面的方法對大多數(shù)枚舉類型來說足夠了,但有時你需要將本質(zhì)上不同的行為與每個常量關(guān)聯(lián)起來。這時通常需要在枚舉類型中聲明一個抽象的apply方法,并在特定于常量的類主題中實現(xiàn)這個方法。這個方法被稱作特定于常量的方法實現(xiàn)。例如:

public enum Operation {
    PULS("+") {
        double apply(double x, double y) { return x + y; } //必須實現(xiàn)
    },
    MINUS("-") {
        double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        double apply(double x, double y) { return x / y; }
    };

    private final String symbol;
    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() { return symbol; }

    abstract double apply(double x, double y);

    public static void main(String[] args) {
        double x = 2.0;
        double y = 4.0;
        for(Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x,y));
    }
}

枚舉類型中的抽象方法,在它的常量中必須被實現(xiàn)。除了編譯時常量之外,枚舉構(gòu)造器不可以訪問枚舉的靜態(tài)域,因為構(gòu)造器運行時,靜態(tài)域還沒被初始化。

特定于常量的方法,使得在枚舉常量中共享代碼變的更加困難。例如:根據(jù)給定的工人的基本工資(按小時算)和工作時間,用枚舉計算工人當(dāng)天的工作報酬。其中加班工資為平時的1.5倍。

public enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURADAY, FRIDAY, SATURDAY, SUNDAY;

    private static final int HOURS_PER_SHIFT = 8;

    double pay(double hoursWorked, double payRate) {
        switch(this) {
            case SATURDAY: case SUNDAY :
                return hoursWorked*payRate*1.5;
            default :
                return hoursWorked - HOURS_PER_SHIFT > 0 
                    ?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate) 
                    : hoursWorked*payRate;
        }
    }

    public static void main(String[] args) {
        System.out.println(PayrollDay.MONDAY.pay(10,10));
        System.out.println(PayrollDay.SUNDAY.pay(10,10));
    }
}

上面這段代碼雖然十分簡潔,但是維護成本很高。每將一個元素添加到該枚舉中,就必須修改switch語句。可以使用策略枚舉來進行優(yōu)化,例如:

public enum PayrollDay {
    MONDAY(PayType.WEEKDAY), 
    TUESDAY(PayType.WEEKDAY), 
    WEDNESDAY(PayType.WEEKDAY), 
    THURADAY(PayType.WEEKDAY), 
    FRIDAY(PayType.WEEKDAY), 
    SATURDAY(PayType.WEEKEND),
    SUNDAY(PayType.WEEKEND);

    private final PayType payType;
    PayrollDay(PayType payType) {
        this.payType = payType;
    }

    double pay(double hoursWorked, double payRate) {
        return payType.pay(hoursWorked, payRate);
    }

    //私有嵌套的枚舉類
    private enum PayType {
        WEEKDAY {
            double pay(double hoursWorked, double payRate) {
                return hoursWorked - HOURS_PER_SHIFT > 0 
                    ?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate) 
                    : hoursWorked*payRate;
            }
        },
        WEEKEND {
            double pay(double hoursWorked, double payRate) {
                return hoursWorked * payRate * 1.5;
            }
        };
        private static final int HOURS_PER_SHIFT = 8;

        abstract double pay(double hoursWorked, double payRate);
    }

    public static void main(String[] args) {
        System.out.println(PayrollDay.MONDAY.pay(10,10));
        System.out.println(PayrollDay.SUNDAY.pay(10,10));
    }
}

總之,與int常量相比,枚舉類型優(yōu)勢明顯。許多枚舉都不需要顯式的構(gòu)造器或成員。當(dāng)需要將不同的行為與每個常量關(guān)聯(lián)起來時,可使用特定于常量的方法。若多個枚舉常量同時共享相同的行為,考慮使用策略枚舉。

31、用實例域代替序數(shù)

所有的枚舉都有一個ordinal方法,它返回枚舉常量在類中的位置。若常量進行重排序,它們ordinal的返回值將發(fā)生變化。所以,永遠不要根據(jù)枚舉的序數(shù)導(dǎo)出與它關(guān)聯(lián)的值,而是要將它保存在一個實例域中。

public enum Planet {
    MERCURY(1),
    VENUS(2),
    EARTH(3),
    MARS(4),
    JUPITER(5),
    SATURN(6),
    URANUS(7),
    NEPTUNE(8);

    private final int numOrd;
    Planet(int numOrd) {this.numOrd = numOrd; }

    public int numOrd(){ return numOrd; }
}

Enum規(guī)范中談到ordinal時寫道:它是用于像EnumSet和EnumMap這種基于枚舉的數(shù)據(jù)結(jié)構(gòu)的方法,平時最好不要使用它。

32、用EnumSet代替位域

若枚舉類型要用在集合中,可以使用EnumSet類。EnumSet類是專為枚舉類設(shè)計的集合類,EnumSet中的所有元素都必須是單個枚舉類型中的枚舉值。若元素個數(shù)小于64,整個EnumSet就用一個long來表示,所以它的性能比的上位域(通過位操作實現(xiàn))的性能。

import java.util.*;

public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

    // Any Set could be passed in, but EnumSet is clearly best
    public void applyStyles(Set<Style> styles) {
        // Body goes here
    }

    // Sample use
    public static void main(String[] args) {
        Text text = new Text();
        text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
    }
}

EnumSet類集位域的簡潔和性能優(yōu)勢及枚舉的所有優(yōu)點與一身。y應(yīng)使用EnumSet代替位域操作。

33、用EnumMap代替序數(shù)索引

EnumMap是一種鍵值必須為枚舉類型的映射表。雖然使用其它的Map實現(xiàn)(如HashMap)也能完成枚舉類型實例到值的映射,但是使用EnumMap會更加高效。由于枚舉類型實例的數(shù)量相對固定并且有限,所以EnumMap使用數(shù)組來存放與枚舉類型對應(yīng)的值,這使得EnumMap的效率比其它的Map實現(xiàn)(如HashMap也能完成枚舉類型實例到值的映射)更高。

注意:EnumMap在內(nèi)部使用枚舉類型的ordinal()得到當(dāng)前實例的聲明次序,并使用這個次序維護枚舉類型實例對應(yīng)值在數(shù)組中的位置。

例如:


import java.util.*;
public class DatabaseInfo {
    private enum DBType { MYSQL, ORACLE, SQLSERVER }

    private static final EnumMap<DBType, String> urls 
        = new EnumMap<>(DBType.class);

    static {
        urls.put(DBType.MYSQL, "jdbc:mysql://localhost/mydb");
        urls.put(DBType.ORACLE, "jdbc:oracle:thin:@localhost:1521:sample");
        urls.put(DBType.SQLSERVER, "jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=mydb");
    }

    private DatabaseInfo() {}

    public static String getURL(DBType type) {
        return urls.get(type);
    }

    public static void main(String[] args) {
        System.out.println(DatabaseInfo.getURL(DBType.SQLSERVER));
        System.out.println(DatabaseInfo.getURL(DBType.MYSQL));
    }
}

不要用序數(shù)(ordinal方法)來索引數(shù)組,而要使用EnumMap。若所表示的關(guān)系是多維的,可以使用EnumMap<.., EnumMap<..>>。一般情況下不要使用Enum.ordinal

34、用接口模擬可伸縮的枚舉

枚舉類型都默認(rèn)繼承自java.lang.Enum類。雖然無法編寫可擴展的枚舉類型,卻可以通過編寫接口以及實現(xiàn)該接口的基礎(chǔ)枚舉類型,以實現(xiàn)對程序的擴展。

例如:


import java.util.*;
//測試
public class Test {

    public 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 = 2.0;
        double y = 4.0;
        test(ExtendedOperation.class, x, y);
        test(BasicOperation.class, x, y);

    }
}

//接口
interface Operation {
    double apply(double x, double y); //默認(rèn)為public的
}

//基本操作,實現(xiàn)Operation接口
enum BasicOperation implements Operation{
    PULS("+") {
        //訪問權(quán)限必須為public,否則報錯
        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; }
}

//擴展操作,實現(xiàn)Operation接口
enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) { return Math.pow(x,y); } //必須實現(xiàn)
    },
    REMAINDER("%") {
        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; }
}

其中<T extends Enum<T> & Operation>確保了Class對象既是枚舉類型又是Operation的子類型。

35、注解優(yōu)先于命名模式

java1.5之前,一般使用命名模式來對程序進行特殊處理。如,JUnit測試框架要求用test作為測試方法名稱的開頭。使用命名模式有幾個缺點:

  • 文字拼寫錯誤會導(dǎo)致失敗,且沒有任何提示。如,test寫成tset
  • 無法確保它們只用于相應(yīng)的程序元素上。如,變量名使用test開頭
  • 沒有提供將參數(shù)值與程序元素關(guān)聯(lián)起來的方法

注解很好的解決了這些問題。關(guān)于注解的詳細(xì)用法請看 java基礎(chǔ)(二),Annotation(注解)

例如:

import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ExceptionTest {
    Class<? extends Exception>[] value();
}

class Sample {
    @ExceptionTest( { IndexOutOfBoundsException.class,
         NullPointerException.class})
    public static void doublyBad() {
        //List<String> list = new ArrayList<>();
        List<String> list = null;
        list.add(5,null);
    }
}

public class RunTest {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName("Sample");
        for(Method m : testClass.getDeclaredMethods()) {
            if(m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try{
                    m.invoke(null);
                }catch (InvocationTargetException ite) {
                    //Throwable exc = ite.getTargetException();
                    Throwable exc = ite.getCause();
                    Class<? extends Exception>[] excTypes 
                        = m.getAnnotation(ExceptionTest.class).value();
                    for(Class<? extends Exception> excType : excTypes) {
                        if(excType.isInstance(exc)) {
                            excType.newInstance().printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

在利用 Method 對象的 invoke 方法調(diào)用目標(biāo)對象的方法時, 若在目標(biāo)對象的方法內(nèi)部拋出異常, 會拋出 InvocationTargetException 異常, 該異常包裝了目標(biāo)對象的方法內(nèi)部拋出異常, 可以通過調(diào)用 InvocationTargetException 異常類的的 getTargetException() 方法得到原始的異常.

在編寫一個需要程序員給源文件添加信息的工具時,應(yīng)該定義一組適當(dāng)?shù)淖⒔猓皇鞘褂妹J健?/p>

36、堅持使用Override注解

@Override注解只能用在方法聲明中,它表示被注解的方法聲明覆蓋了超類型中的一個聲明。使用Override注解可以有效防止覆蓋方法時的錯誤。

例如:想要在String中覆蓋equals方法

//這是方法重載,將產(chǎn)生編譯錯誤
@Override
public boolean equals(String obj) {
    ....
}

//覆蓋
@Override
public boolean equals(Object obj) {
    ....
}

37、用標(biāo)記接口定義類型

標(biāo)記接口是指沒有任何屬性和方法的接口,它只用來表明類實現(xiàn)了某種屬性。如,Serializable接口,通過實現(xiàn)這個接口,類表明它的實例可以被寫到ObjectOutputStream。標(biāo)記注解是特殊類型的注解,其中不包含成員。標(biāo)記注解的唯一目的就是標(biāo)記聲明。

  • 標(biāo)記接口的優(yōu)點:標(biāo)記接口允許你在編譯時捕捉在使用標(biāo)記注解的情況下要到運行時才能捕捉到的錯誤。
  • 標(biāo)記注解的優(yōu)點:便于擴展,可以給已被使用的注解類型添加更多信息(元注解)。而接口實現(xiàn)后不可能再添加方法。

標(biāo)記接口與標(biāo)記注解如何選擇:

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

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

  • Java 1.5發(fā)行版本新增了兩個引用類型家族:枚舉類型(Enumerate類)和注解類型(Annotation接...
    Timorous閱讀 418評論 0 0
  • 經(jīng)典重讀——亞馬遜鏈接 筆記鏈接 導(dǎo)圖: 筆記文本: Effective Java1 第2章 創(chuàng)建和銷毀對象1.1...
    8c3c932b5ffd閱讀 1,440評論 0 1
  • 對象的創(chuàng)建與銷毀 Item 1: 使用static工廠方法,而不是構(gòu)造函數(shù)創(chuàng)建對象:僅僅是創(chuàng)建對象的方法,并非Fa...
    孫小磊閱讀 2,015評論 0 3
  • 創(chuàng)建和銷毀對象 靜態(tài)工廠模式 構(gòu)造器里未傳參的成員不會被初始化。int類型是0,布爾類型是false,String...
    揚州慢_閱讀 490評論 0 5
  • 每個不甘于平凡的靈魂背后想必都有一個創(chuàng)業(yè)的夢想,曾經(jīng)的我也曾一度迷茫,直到三個月前我了解了跨境電商,這一領(lǐng)域所特有...
    自信的風(fēng)閱讀 5,427評論 0 1