1. 枚舉基本特征
關鍵詞enum可以將一組具名值的有限集合創建成一種新的類型,而這些具名的值可以作爲常規程序組件使用。
枚舉最常見的用途便是替換常量定義,爲其增添類型約束,完成編譯時類型驗證。
1.1. 枚舉定義
枚舉的定義與類和常量定義非常類型。使用enum關鍵字替換class關鍵字,然後在enum中定義“常量”即可。
例如,需要將用戶分爲“可用”和“禁用”兩種狀態,爲了達到定義的統一管理,一般會使用常量進行說明,如下:
public class UserStatusConstants { public static final int ENABLE = 0; public static final int DISABLE = 1; } public class User1 { private String name; private int status;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; } }
在User1中,描述用戶狀態的類型爲int,(期望可用值爲0和1,但實際可選擇值爲int),從使用方角度出發,需要事先知道UserStatusConstants中定義的常量才能保障調用的准確性,setStatus和getStatus兩個方法繞過了Java類型檢測。
接下來,我們看一下枚舉如何優化這個問題,enum方案如下:
public enum UserStatus { ENABLE, DISABLE; } public class User2 { private String name; private UserStatus status;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; } } @Test public void useUserStatus(){ User2 user = new User2(); user.setName(“test”);
user.setStatus(UserStatus.DISABLE); user.setStatus(UserStatus.ENABLE); }
getStatus和setStatus所需類型爲UserStatus,不在是比較寬泛的int。在使用的時候可以通過UserStatus.XXX的方式獲取對用的枚舉值。
1.2. 枚舉的秘密
1.2.1. 枚舉的單例性
枚舉值具有單例性,及枚舉中的每個值都是一個單例對象,可以直接使用==進行等值判斷。
枚舉是定義單例對象最簡單的方法。
1.2.2. name 和 ordrial
對于簡單的枚舉,存在兩個維度,一個是name,即爲定義的名稱;一個是ordinal,即爲定義的順序。
name測試如下:
@Test public void nameTest(){ for (UserStatus userStatus : UserStatus.values()){ // 枚舉的name維度 String name = userStatus.name(); System.out.println(“UserStatus:” + name);
// 通過name獲取定義的枚舉 UserStatus userStatus1 = UserStatus.valueOf(name); System.out.println(userStatus == userStatus1); } }
輸出結果爲:
UserStatus:ENABLE true UserStatus:DISABLE true
ordrial測試如下:
@Test public void ordinalTest(){ for (UserStatus userStatus : UserStatus.values()){ // 枚舉的ordinal維度 int ordinal = userStatus.ordinal(); System.out.println(“UserStatus:” + ordinal);
// 通過ordinal獲取定義的枚舉 UserStatus userStatus1 = UserStatus.values()[ordinal]; System.out.println(userStatus == userStatus1); } }
1.2.3. 枚舉的本質
創建enum時,編譯器會爲你生成一個相關的類,這個類繼承自java.lang.Enum。
先看下Enum提供了什麽:
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { // 枚舉的Name維度 private final String name; public final String name() { return name; } // 枚舉的ordinal維度 private final int ordinal; public final int ordinal() { return ordinal; }
// 枚舉構造函數 protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
/** * 重寫toString方法, 返回枚舉定義名稱 */ public String toString() { return name; } // 重寫equals,由于枚舉對象爲單例,所以直接使用==進行比較 public final boolean equals(Object other) { return this==other; } // 重寫hashCode public final int hashCode() { return super.hashCode(); }
/** * 枚舉爲單例對象,不允許clone */ protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
/** * 重寫compareTo方法,同種類型按照定義順序進行比較 */ public final int compareTo(E o) { Enum<?> other = (Enum<?>)o; Enum<E> self = this; if (self.getClass() != other.getClass() && // optimization self.getDeclaringClass() != other.getDeclaringClass()) throw new ClassCastException(); return self.ordinal – other.ordinal; }
/** * 返回定義枚舉的類型 */ @SuppressWarnings(“unchecked”) public final Class<E> getDeclaringClass() { Class<?> clazz = getClass(); Class<?> zuper = clazz.getSuperclass(); return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper; }
/** * 靜態方法,根據name獲取枚舉值 * @since 1.5 */ public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException(“Name is null”); throw new IllegalArgumentException( “No enum constant ” + enumType.getCanonicalName() + “.” + name); }
protected final void finalize() { }
/** * 枚舉爲單例對象,禁用反序列化 */ 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”); } }
從Enum中我們可以得到:
- Enum中對name和ordrial(final)的屬性進行定義,並提供構造函數進行初始化
- 重寫了equals、hashCode、toString方法,其中toString方法默認返回name
- 實現了Comparable接口,重寫compareTo,使用枚舉定義順序進行比較
- 實現了Serializable接口,並重寫禁用了clone、readObject等方法,以保障枚舉的單例性
- 提供valueOf方法使用反射機制,通過name獲取枚舉值
到此已經解釋了枚舉類的大多數問題,UserStatus.values(), UserStatus.ENABLE, UserStatus.DISABLE,又是從怎麽來的呢?這些是編譯器爲其添加的。
@Test public void enumTest(){ System.out.println(“Fields”);
for (Field field : UserStatus.class.getDeclaredFields()){ field.getModifiers(); StringBuilder fieldBuilder = new StringBuilder(); fieldBuilder.append(Modifier.toString(field.getModifiers())) .append(” “) .append(field.getType()) .append(” “) .append(field.getName());
System.out.println(fieldBuilder.toString()); }
System.out.println(); System.out.println(“Methods”); for (Method method : UserStatus.class.getDeclaredMethods()){ StringBuilder methodBuilder = new StringBuilder(); methodBuilder.append(Modifier.toString(method.getModifiers())); methodBuilder.append(method.getReturnType()) .append(” “) .append(method.getName()) .append(“(“); Parameter[] parameters = method.getParameters(); for (int i=0; i< method.getParameterCount(); i++){ Parameter parameter = parameters[i]; methodBuilder.append(parameter.getType()) .append(” “) .append(parameter.getName()); if (i != method.getParameterCount() -1) { methodBuilder.append(“,”); } } methodBuilder.append(“)”); System.out.println(methodBuilder); } }
我們分別對UserStatus中的屬性和方法進行打印,結果如下:
Fields public static final class com.example.enumdemo.UserStatus ENABLE public static final class com.example.enumdemo.UserStatus DISABLE private static final class [Lcom.example.enumdemo.UserStatus; $VALUES
Methods public staticclass [Lcom.example.enumdemo.UserStatus; values() public staticclass com.example.enumdemo.UserStatus valueOf(class java.lang.String arg0)
由輸出,我們可知編譯器爲我們添加了以下幾個特性:
- 針對每一個定義的枚舉值,添加一個同名的public static final的屬性
- 添加一個private static final $VALUES屬性記錄枚舉中所有的值信息
- 添加一個靜態的values方法,返回枚舉中所有的值信息($VALUES)
- 添加一個靜態的valueOf方法,用于通過name獲取枚舉值(調用Enum中的valueOf方法)
2. 枚舉一種特殊的類
雖然編譯器爲枚舉添加了很多功能,但究其本質,枚舉終究是一個類。除了不能繼承自一個enum外,我們基本上可以將enum看成一個常規類,因此屬性、方法、接口等在枚舉中仍舊有效。
2.1. 枚舉中的屬性和方法
除了編譯器爲我們添加的方法外,我們也可以在枚舉中添加新的屬性和方法,甚至可以有main方法。
public enum CustomMethodUserStatus { ENABLE(“可用”), DISABLE(“禁用”);
private final String descr;
private CustomMethodUserStatus(String descr) { this.descr = descr; }
public String getDescr(){ return this.descr; }
public static void main(String… args){ for (CustomMethodUserStatus userStatus : CustomMethodUserStatus.values()){ System.out.println(userStatus.toString() + “:” + userStatus.getDescr()); } } }
main執行輸出結果:
ENABLE:可用 DISABLE:禁用
如果准備添加自定義方法,需要在enum實例序列的最後添加一個分號。同時java要求必須先定義enum實例,如果在定義enum實例前定義任何屬性和方法,那麽在編譯過程中會得到相應的錯誤信息。
enum中的構造函數和普通類沒有太多的區別,但由于只能在enum中使用構造函數,其默認爲private,如果嘗試升級可見範圍,編譯器會給出相應錯誤信息。
2.2. 重寫枚舉方法
枚舉中的方法與普通類中方法並無差別,可以對其進行重寫。其中Enum類中的name和ordrial兩個方法爲final,無法重寫。
public enum OverrideMethodUserStatus { ENABLE(“可用”), DISABLE(“禁用”);
private final String descr;
OverrideMethodUserStatus(String descr) { this.descr = descr; }
@Override public String toString(){ return this.descr; }
public static void main(String… args){ for (OverrideMethodUserStatus userStatus : OverrideMethodUserStatus.values()){ System.out.println(userStatus.name() + “:” + userStatus.toString()); } } }
main輸出結果爲
ENABLE:可用 DISABLE:禁用
重寫toString方法,返回描述信息。
2.3. 接口實現
由于所有的enum都繼承自java.lang.Enum類,而Java不支持多繼承,所以我們的enum不能再繼承其他類型,但enum可以同時實現一個或多個接口,從而對其進行擴展。
public interface CodeBasedEnum { int code(); } public enum CodeBasedUserStatus implements CodeBasedEnum{ ENABLE(1), DISABLE(0);
private final int code;
CodeBasedUserStatus(int code) { this.code = code; }
@Override public int code(){ return this.code; }
public static void main(String… arg){ for (CodeBasedUserStatus codeBasedEnum : CodeBasedUserStatus.values()){ System.out.println(codeBasedEnum.name() + “:” + codeBasedEnum.code()); } } }
main函數輸出結果:
ENABLE:1 DISABLE:0 3. 枚舉集合
針對枚舉的特殊性,java類庫對enum的集合提供了支持(主要是圍繞ordinal特性進行優化)。
3.1. EnumSet
Set是一種集合,只能向其中添加不重複的對象。Java5中引入了EnumSet對象,其內部使用long值作爲比特向量,以最大化Set的性能。
EnumSet存在兩種實現類:
- RegularEnumSet 針對枚舉數量小于等于64的EnumSet實現,內部使用long作爲存儲。
- JumboEnumSet 針對枚舉數量大于64的EnumSet實現,內部使用long數組進行存儲。
public enum UserStatus { A1, A2 ; } public enum MoreUserStatus { A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, A24, A25, A26, A27, A28, A29, A30, A31, A32, A33, A34, A35, A36, A37, A38, A39, A40, A41, A42, A43, A44, A45, A46, A47, A48, A49, A50, A51, A52, A53, A54, A55, A56, A57, A58, A59, A60, A61, A62, A63, A64, A65, A66, A67, A68, A69, A70, A71, A72, A73, A74, A75, A76, A77, A78, A79, A80, A81, A82, A83, A84, A85, A86, A87, A88, A89, A90, A91, A92, A93, A94, A95, A96, A97, A98, A99, A100; }
@Test public void typeTest(){ EnumSet<UserStatus> userStatusesSet = EnumSet.noneOf(UserStatus.class); System.out.println(“userStatusSet:” + userStatusesSet.getClass());
EnumSet<MoreUserStatus> moreUserStatusesSet = EnumSet.noneOf(MoreUserStatus.class); System.out.println(“moreUserStatusesSet:” + moreUserStatusesSet.getClass()); }
輸出結果爲:
userStatusSet:class java.util.RegularEnumSet moreUserStatusesSet:class java.util.JumboEnumSet
EnumSet作爲工廠類,提供大量的靜態方法,以方便的創建EnumSet:
- noneOf 創建EnumSet,空Set,沒有任何元素
- allOf 創建EnumSet,滿Set,所有元素都在其中
- copyOf 從已有Set中創建EnumSet
- of 根據提供的枚舉值創建EnumSet
- range 根據枚舉定義的順序,定義一個區間的EnumSet
3.2. EnumMap
EnumMap是一個特殊的Map,他要求其中的鍵值必須來著一個enum。
EnumMap內部實現:
private transient Object[] vals; public V get(Object key) { return (isValidKey(key) ? unmaskNull(vals[((Enum<?>)key).ordinal()]) : null); }
public V put(K key, V value) { typeCheck(key); int index = key.ordinal(); Object oldValue = vals[index]; vals[index] = maskNull(value); if (oldValue == null) size++; return unmaskNull(oldValue); }
由上可見,EnumMap內部由數組實現(ordrial),以提高Map的操作速度。enum中的每個實例作爲鍵,總是存在,但是如果沒有爲這個鍵調用put方法來存入相應值的話,其對應的值便是null。在使用上,EnumMap與key爲枚舉的Map並無差異。
@Test public void test(){ Map<UserStatus, String> welcomeMap = new EnumMap<>(UserStatus.class); welcomeMap.put(UserStatus.A1, “你好”); welcomeMap.put(UserStatus.A2, “您好”); } 4. 枚舉使用案例
枚舉作爲一種特殊的類,爲很多場景提供了更優雅的解決方案。
4.1. Switch
在Java 1.5之前,只有一些簡單類型(int,short,char,byte)可以用于switch的case語句,我們習慣采用‘常量+case’的方式增加代碼的可讀性,但是丟失了類型系統的校驗。由于枚舉的ordinal特性的存在,可以將其用于case語句。
public class FruitConstant { public static final int APPLE = 1; public static final int BANANA = 2; public static final int PEAR = 3; } // 沒有類型保障 public String nameByConstant(int fruit){ switch (fruit){ case FruitConstant.APPLE: return “蘋果”; case FruitConstant.BANANA: return “香蕉”; case FruitConstant.PEAR: return “梨”; } return “未知”; } public enum FruitEnum { APPLE, BANANA, PEAR; } // 有類型保障 public String nameByEnum(FruitEnum fruit){ switch (fruit){ case APPLE: return “蘋果”; case BANANA: return “香蕉”; case PEAR: return “梨”; } return “未知”; }
4.2. 單例
Java中單例的編寫主要有餓漢式、懶漢式、靜態內部類等幾種方式(雙重鎖判斷存在缺陷),但還有一種簡單的方式是基于枚舉的單例。
public interface Converter<S, T> { T convert(S source); }
// 每一個枚舉值都是一個單例對象 public enum Date2StringConverters implements Converter<Date, String>{ yyyy_MM_dd(“yyyy-MM-dd”), yyyy_MM_dd_HH_mm_ss(“yyyy-MM-dd HH:mm:ss”), HH_mm_ss(“HH:mm:ss”); private final String dateFormat;
Date2StringConverters(String dateFormat) { this.dateFormat = dateFormat; }
@Override public String convert(Date source) { return new SimpleDateFormat(this.dateFormat).format(source); } }
public class ConverterTests { private final Converter<Date, String> converter1 = Date2StringConverters.yyyy_MM_dd; private final Converter<Date, String> converter2 = Date2StringConverters.yyyy_MM_dd_HH_mm_ss; private final Converter<Date, String> converter3 = Date2StringConverters.HH_mm_ss;
public void formatTest(Date date){ System.out.println(converter1.convert(date)); System.out.println(converter2.convert(date)); System.out.println(converter3.convert(date));
} }
4.3. 狀態機
狀態機是解決業務流程中的一種有效手段,而枚舉的單例性,爲構建狀態機提供了便利。
以下是一個訂單的狀態扭轉流程,所涉及的狀態包括Created、Canceled、Confirmed、Overtime、Paied;所涉及的動作包括cancel、confirm、timeout、pay。
create
confirm
cancel
cancel
timeout
pay
開始
Created
Confirmed
Canceld
Overtime
Paied
// 狀態操作接口,管理所有支持的動作 public interface IOrderState { void cancel(OrderStateContext context);
void confirm(OrderStateContext context);
void timeout(OrderStateContext context);
void pay(OrderStateContext context); }
// 狀態機上下文 public interface OrderStateContext { void setStats(OrderState state); }
// 訂單實際實現 public class Order{ private OrderState state;
private void setStats(OrderState state) { this.state = state; } // 將請求轉發給狀態機 public void cancel() { this.state.cancel(new StateContext()); }
// 將請求轉發給狀態機 public void confirm() { this.state.confirm(new StateContext()); }
// 將請求轉發給狀態機 public void timeout() { this.state.timeout(new StateContext()); }
// 將請求轉發給狀態機 public void pay() { this.state.pay(new StateContext()); }
// 內部類,實現OrderStateContext,回寫Order的狀態 class StateContext implements OrderStateContext{
@Override public void setStats(OrderState state) { Order.this.setStats(state); } } }
// 基于枚舉的狀態機實現 public enum OrderState implements IOrderState{ CREATED{ // 允許進行cancel操作,並把狀態設置爲CANCELD @Override public void cancel(OrderStateContext context){ context.setStats(CANCELD); }
// 允許進行confirm操作,並把狀態設置爲CONFIRMED @Override public void confirm(OrderStateContext context) { context.setStats(CONFIRMED); }
}, CONFIRMED{ // 允許進行cancel操作,並把狀態設置爲CANCELD @Override public void cancel(OrderStateContext context) { context.setStats(CANCELD); }
// 允許進行timeout操作,並把狀態設置爲OVERTIME @Override public void timeout(OrderStateContext context) { context.setStats(OVERTIME); }
// 允許進行pay操作,並把狀態設置爲PAIED @Override public void pay(OrderStateContext context) { context.setStats(PAIED); }
}, // 最終狀態,不允許任何操作 CANCELD{
}, // 最終狀態,不允許任何操作 OVERTIME{
}, // 最終狀態,不允許任何操作 PAIED{
};
@Override public void cancel(OrderStateContext context) { throw new NotSupportedException(); }
@Override public void confirm(OrderStateContext context) { throw new NotSupportedException(); }
@Override public void timeout(OrderStateContext context) { throw new NotSupportedException(); }
@Override public void pay(OrderStateContext context) { throw new NotSupportedException(); } }
4.4. 責任鏈
在責任鏈模式中,程序可以使用多種方式來處理一個問題,然後把他們鏈接起來,當一個請求進來後,他會遍曆整個鏈,找到能夠處理該請求的處理器並對請求進行處理。
枚舉可以實現某個接口,加上其天生的單例特性,可以成爲組織責任鏈處理器的一種方式。
// 消息類型 public enum MessageType { TEXT, BIN, XML, JSON; }
// 定義的消息體 @Value public class Message { private final MessageType type; private final Object object;
public Message(MessageType type, Object object) { this.type = type; this.object = object; } }
// 消息處理器 public interface MessageHandler { boolean handle(Message message); }
// 基于枚舉的處理器管理 public enum MessageHandlers implements MessageHandler{ TEXT_HANDLER(MessageType.TEXT){ @Override boolean doHandle(Message message) { System.out.println(“text”); return true; } }, BIN_HANDLER(MessageType.BIN){ @Override boolean doHandle(Message message) { System.out.println(“bin”); return true; } }, XML_HANDLER(MessageType.XML){ @Override boolean doHandle(Message message) { System.out.println(“xml”); return true; } }, JSON_HANDLER(MessageType.JSON){ @Override boolean doHandle(Message message) { System.out.println(“json”); return true; } };
// 接受的類型 private final MessageType acceptType;
MessageHandlers(MessageType acceptType) { this.acceptType = acceptType; } // 抽象接口 abstract boolean doHandle(Message message); // 如果消息體是接受類型,調用doHandle進行業務處理 @Override public boolean handle(Message message) { return message.getType() == this.acceptType && doHandle(message); } }
// 消息處理鏈 public class MessageHandlerChain { public boolean handle(Message message){ for (MessageHandler handler : MessageHandlers.values()){ if (handler.handle(message)){ return true; } } return false; } }
4.5. 分發器
分發器根據輸入的數據,找到對應的處理器,並將請求轉發給處理器進行處理。 由于EnumMap其出色的性能,特別適合根據特定類型作爲分發策略的場景。
// 消息體 @Value public class Message { private final MessageType type; private final Object data;
public Message(MessageType type, Object data) { this.type = type; this.data = data; } }
// 消息類型 public enum MessageType { // 登錄 LOGIN, // 進入房間 ENTER_ROOM, // 退出房間 EXIT_ROOM, // 登出 LOGOUT; }
// 消息處理器 public interface MessageHandler { void handle(Message message); }
// 基于EnumMap的消息分發器 public class MessageDispatcher { private final Map<MessageType, MessageHandler> dispatcherMap = new EnumMap<MessageType, MessageHandler>(MessageType.class);
public MessageDispatcher(){ dispatcherMap.put(MessageType.LOGIN, message -> System.out.println(“Login”)); dispatcherMap.put(MessageType.ENTER_ROOM, message -> System.out.println(“Enter Room”));
dispatcherMap.put(MessageType.EXIT_ROOM, message -> System.out.println(“Exit Room”)); dispatcherMap.put(MessageType.LOGOUT, message -> System.out.println(“Logout”)); }
public void dispatch(Message message){ MessageHandler handler = this.dispatcherMap.get(message.getType()); if (handler != null){ handler.handle(message); } } } 5. 總結
枚舉本身並不複雜,主要理解編譯器爲我們所做的功能加強。究其本質,枚舉只是一個特殊的類型,除了不能繼承父類之外,擁有類的一切特性;加之其天生的單例性,可以有效的應用于一些特殊場景。