重走JAVA編程之路(一)枚舉

Java 1.5 發行版本增長了新的引用類型: 枚舉, 在其以前,咱們使用枚舉類型值的時候一般是藉助常量組成合法值的類型,例如表示光的三原色:紅黃藍的代碼表示多是如下這樣的。java

/*******************光的三原色*********************/
    public static final int LIGHT_RED = 1;
    public static final int LIGHT_YELLOW = 2;
    public static final int LIGHT_BLUE = 3;

    /*******************顏料的三原色*********************/
    public static final int PIGMENT_RED = 1;
    public static final int PIGMENT_YELLOW = 2;
    public static final int PIGMENT_BLUE = 3;
複製代碼

可是這樣使用功能是受限的,好比不能知道對應枚舉的個數等。幸虧,Java 1.5引入了枚舉類型Enum。設計模式

枚舉的特性

Java 的枚舉類型的父類均爲 java.lang.Enum

Java的枚舉本質上是int值

使用枚舉類型將前面的使用常量方式調整以下:數組

public enum LighjtOriginColorEnums {
    RED,
    YELLOW,
    BLUE
}

public enum PigmentOriginColorEnums {
    RED,
    YELLOE,
    BLUE;
}

public static void main(String[] args){
    for(LighjtOriginColorEnums ele : LighjtOriginColorEnums.values()){
        System.out.println(ele + " int value is: " + ele.ordinal());
    }
}

複製代碼

輸出結果爲:安全

RED int value is: 0
YELLOW int value is: 1
BLUE int value is: 2
複製代碼

枚舉類型的 ordinal()方法,能夠獲得枚舉的int整型值,該方法在 Enum中定義,是一個不可覆蓋的方法:bash

public final int ordinal() {
    return ordinal;
}
複製代碼

該方法返回枚舉的 ordinal屬性值, 該值默認是枚舉在其定義中的未知的索引值, 從0開始,即 RED.ordinal = 0, YELLOW.ordinal = 1, BLUE.ordinal = 2。ordinal的值大都數狀況下是不會用到的。app

枚舉不能被實例化

關於枚舉的說明:ide

枚舉是不能實例化的,只能聲明後,再使用post

枚舉不能實例化,因此是單例的性能

同理,枚舉也是線程安全的ui

因此可使用枚舉實現單例模式

枚舉提供了編譯時的類型安全

若是聲明一個參數類型爲 LighjtOriginColorEnums,則能夠保證傳遞到該方法的任何非 null 的參數必須爲 LighjtOriginColorEnums 中的三個枚舉值之一。

枚舉禁用了 clone 方法

protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
複製代碼

枚舉不會被finalize

/** * enum classes cannot have finalize methods. */
    protected final void finalize() { }
複製代碼

枚舉禁用了反序列化

/** * prevent default deserialization */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
複製代碼

正由於枚舉具備這些特性,因此咱們可使用枚舉實現友好的單例模式,請見: 【設計模式】你的單例模式真的是生產可用的嗎?

枚舉能夠定義方法

看一個簡單的示例,表示一個互金公司的金額項:本金、利息、手續費、滯納金 的枚舉: acquireOffsetItemByCode(String code)方法能夠根據枚舉的code屬性,得到枚舉。

public enum OffsetItemEnums {

    /** * 手續費 */
    offsetitem_fee("fee", "手續費"),
    /** * 滯納金 */
    offsetitem_penalty("penalty", "滯納金"),
    /** * 利息 */
    offsetitem_int("int", "利息"),
    /** * 本金 */
    offsetitem_principal("principal", "本金"),


    ;

    private String code;
    private String desc;

    private OffsetItemEnums(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public static OffsetItemEnums acquireOffsetItemByCode(String code) throws Exception{
        for( OffsetItemEnums ele : OffsetItemEnums.values() ){
            if( ele.code.equalsIgnoreCase(code) ){
                return ele;
            }
        }
        throw new Exception("Error code:" + code);
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

複製代碼

Effective Java 中的場景實例: 枚舉中的抽象方法

在 Effective Java 第二版中的第30條定律中,舉例了一個場景,如實現四則運算。

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;

    double apply(double x, double y) throws Exception{
        switch (this){
            case PLUS:
                return x + y;
            case MINUS:
                return x - y;
            case TIMES:
                return x * y;
            case DIVIDE:
                return x / y;
        }
        throw new Exception("Unknown op: " + this);
    }
}
複製代碼

這段代碼可行, 可是須要 throw 一個Exception,若是沒有拋出一個異常,就編譯不過了,可是其實是不可能執行到最後一行代碼的。還存在一個問題是,若是新增了枚舉常量,可是忘記給switch添加相應的條件,枚舉仍然能夠編譯,可是運行時不會獲得指望的結果。

咱們發現一種更好的實現,是給枚舉添加一個抽象的方法 apply:

public enum OperationGraceful {
    PLUS, MINUS, TIMES, DIVIDE;

    abstract double apply(double x, double y);
}
複製代碼

此時編譯錯誤: Class 'OperationGraceful' must either be declared abstract or implement abstract method 'apply(double, double)' in 'OperationGraceful' 意思是每個枚舉常量必須實現聲明的抽象方法:

public enum OperationGraceful {
    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);
}
複製代碼

如此一來,就不會在新增一個枚舉值後,遺漏掉前面示例的switch分支邏輯,即便忘記了,編譯器也會提醒,您須要實現抽象方法的規約。

特定於常量的方法實現能夠與特定於常量的數據結合起來:

public enum OperationGracefulField {
    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;
        }
    };

    // 屬性
    private String symbol;
    OperationGracefulField(String symbol){
        this.symbol = symbol;
    }

    abstract double apply(double x, double y);
}

複製代碼

計算加班工資的案例

在五個工做日中,正常超過8個小時,就會產生加班工資(固然現實實際上是不可能的,萬惡的資本主義丿_\)。週末所有算加班。加班費按基本工資的一半計算。

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

    private static final int HOUR_PER_SHIFT = 8;

    double pay(double hourseWorked, double payRate){
        double basePay = hourseWorked * payRate;

        double overtimePay;

        switch (this){
            case SATURDAY:
            case SUNDAY:
                overtimePay = hourseWorked * payRate / 2;
             default:
                 overtimePay = hourseWorked <= HOUR_PER_SHIFT
                         ? 0 : (hourseWorked - HOUR_PER_SHIFT) * payRate / 2;
                 break;
        }

        return  basePay + overtimePay;
    }
}
複製代碼

這個程序能夠運行,達到基本的業務需求,可是從維護角度來看,比較危險,假設將一個元素添加到枚舉中,或許是一個表示假期天數的特殊值,可是很是不幸,忘記了在switch分支添加代碼區分,雖然程序能夠編譯,可是實際運行時可能會出現尷尬的結果,好比假期也計算了工資,帶薪假期忘了計算工資等等。

策略枚舉的計算加班工資實現

咱們能夠根據是否工做日、雙休日,將加班工資計算移到一個私有的嵌套枚舉中,而後將這個 策略枚舉 的實例傳遞到 PayrollDay 枚舉的構造器中。 以後 PayrollDay 的加班費計算規則委託給策略枚舉, PayrollDay 就不須要switch 語句或者特定於常量的方法了。

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

    private final PayType payType;

    PayrollDayStrategy(PayType payType){
        this.payType = payType;
    }

    private enum PayType{
        WEEKDAY{
            @Override
            double overtimePay(double hrs, double payRate) {
                return hrs <= HOUR_PER_SHIFT ?
                        0 : (hrs - HOUR_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            @Override
            double overtimePay(double hrs, double payRate) {
                return hrs * payRate /2;
            }
        };

        private static final int HOUR_PER_SHIFT = 8;

        abstract double overtimePay(double hrs, double payRate);

        double pay( double hoursWork, double payRate ){
            double basePay = hoursWork * payRate;
            return basePay + overtimePay(hoursWork, payRate);
        }
    }
}
複製代碼

枚舉的使用場景

  • 須要一組固定常量的時候

    例如: 行星、一週的天數等等

  • 若是多個枚舉常量同時共享相同的行爲, 則考使用慮策略枚舉

EnumSet 枚舉集合

EnumSet類用來有效地表示從單個枚舉類型中提取的多個值的多個集合,實現了 Set 集合,提供了豐富的功能和類型安全性。

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

    public void applyStyle(Set<Style> styles){
        // TODO
    }

    public static void main(String[] args){
        EnumSetDemo enumSetDemo = new EnumSetDemo();
        enumSetDemo.applyStyle(EnumSet.of(Style.BOLD, Style.ITALIC));
    }
}
複製代碼

EnumSet 提供了不少靜態方法用於建立集合。

EnumSet.of(...) 源碼

public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) {
    EnumSet<E> result = noneOf(e1.getDeclaringClass());
    result.add(e1);
    result.add(e2);
    return result;
}
    
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
        throw new ClassCastException(elementType + " not an enum");

    if (universe.length <= 64)
        return new RegularEnumSet<>(elementType, universe);
    else
        return new JumboEnumSet<>(elementType, universe);
}


複製代碼

其中 RegularEnumSetJumboEnumSet是JDK自帶的EnumSet的實現類。 若是底層的枚舉類型有64個或者更少的元素————其實大都數如此,整個 EnumSet 就用單個long來表示,性能較好。

EnumMap 枚舉映射使用場景

自從咱們知道 Enum 存在一個 ordinal 方法以後,可能對其使用就躍躍欲試了,好比,有一個用來表示一種烹飪的香草:

public enum Herb {
    ,
    ;
    
    public enum Type{
        ANNUAL, PERENNIAL, BIENNIAL
    }

    private String name;
    private Type type;

    Herb(String name, Type type){
        this.name = name;
        this.type = type;
    }


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

}
複製代碼

場景 : 如今假設有一個香草的數組,表示一座花園中的植物,可是想要按照類型進行組織後將這些植物列出來。 分析: 咱們發現,須要按類型分類,而後列出來。能夠用做映射,恰好有一個 EnumMap 的類能夠達到這樣的目的。

public enum Herb {
    A("A", Type.ANNUAL),
    B("B", Type.BIENNIAL),
    AA("AA", Type.ANNUAL),
    BB("BB", Type.BIENNIAL),
    C("C", Type.PERENNIAL),
    ;

    public enum Type{
        ANNUAL, PERENNIAL, BIENNIAL
    }

    private String name;
    private Type type;

    Herb(String name, Type type){
        this.name = name;
        this.type = type;
    }


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

    public static void main(String[] args){
        // 這是一個花園,栽種有各類類型的香草
        Herb[] garden = new Herb[]{Herb.A, Herb.B, Herb.AA, Herb.BB, Herb.C};

        // EnumMap 構造器須要指定 class 做爲類型參數
        Map<Herb.Type, Set<Herb>> herbsByType =
                new EnumMap<Type, Set<Herb>>(Type.class);

        for (Type t : Herb.Type.values()){
            // 按類型分類
            herbsByType.put(t, new HashSet<Herb>());
        }

        // 開始對花園處理
        for (Herb h : garden){
            herbsByType.get(h.type).add(h);
        }

        // 輸出分類信息
        System.out.println(herbsByType);
    }

}
複製代碼

枚舉實現接口

枚舉還能夠實現接口,用於實現擴展功能。

改裝前面的四則運算案例:

// 接口
package org.byron4j.cookbook.javacore.enums;

public interface OperationI {
    public double apply(double x, double y);
}



// 實現接口的枚舉
package org.byron4j.cookbook.javacore.enums;

public enum BasicOperation implements OperationI{
    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 String symbol;
    BasicOperation(String symbol){
        this.symbol = symbol;
    }


}

複製代碼

參考資料:

  • Effective Java(第二版)
相關文章
相關標籤/搜索