重走JAVA程式設計之路(一)列舉
Java 1.5 發行版本增加了新的引用型別:列舉 , 在其之前,我們使用列舉型別值的時候通常是藉助常量組成合法值的型別,例如表示光的三原色:紅黃藍的程式碼表示可能是以下這樣的。
/*******************光的三原色*********************/ 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
中定義,是一個不可覆蓋的方法:
public final int ordinal() { return ordinal; } 複製程式碼
該方法返回列舉的ordinal
屬性值, 該值預設是列舉在其定義中的未知的索引值, 從0開始,即 RED.ordinal = 0, YELLOW.ordinal = 1, BLUE.ordinal = 2。ordinal的值大都數情況下是不會用到的。
列舉不能被例項化
關於列舉的說明:
列舉是不能例項化的,只能聲明後,再使用
列舉不能例項化,所以是單例的
同理,列舉也是執行緒安全的
所以可以使用列舉實現單例模式
列舉提供了編譯時的型別安全
如果宣告一個引數型別為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; } returnbasePay + 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); } 複製程式碼
其中RegularEnumSet
、JumboEnumSet
是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(第二版)