Menu
快讀
  • 旅遊
  • 生活
    • 美食
    • 寵物
    • 養生
    • 親子
  • 娛樂
    • 動漫
  • 時尚
  • 社會
  • 探索
  • 故事
  • 科技
  • 軍事
  • 国际
快讀

Java 枚舉 知多少?

2021 年 3 月 11 日 风客会

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中我們可以得到:

  1. Enum中對name和ordrial(final)的屬性進行定義,並提供構造函數進行初始化
  2. 重寫了equals、hashCode、toString方法,其中toString方法默認返回name
  3. 實現了Comparable接口,重寫compareTo,使用枚舉定義順序進行比較
  4. 實現了Serializable接口,並重寫禁用了clone、readObject等方法,以保障枚舉的單例性
  5. 提供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)

由輸出,我們可知編譯器爲我們添加了以下幾個特性:

  1. 針對每一個定義的枚舉值,添加一個同名的public static final的屬性
  2. 添加一個private static final $VALUES屬性記錄枚舉中所有的值信息
  3. 添加一個靜態的values方法,返回枚舉中所有的值信息($VALUES)
  4. 添加一個靜態的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存在兩種實現類:

  1. RegularEnumSet 針對枚舉數量小于等于64的EnumSet實現,內部使用long作爲存儲。
  2. 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:

  1. noneOf 創建EnumSet,空Set,沒有任何元素
  2. allOf 創建EnumSet,滿Set,所有元素都在其中
  3. copyOf 從已有Set中創建EnumSet
  4. of 根據提供的枚舉值創建EnumSet
  5. 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. 總結

枚舉本身並不複雜,主要理解編譯器爲我們所做的功能加強。究其本質,枚舉只是一個特殊的類型,除了不能繼承父類之外,擁有類的一切特性;加之其天生的單例性,可以有效的應用于一些特殊場景。

相關文章:

  • 阿裏問題定位神器 Arthas 操作實踐,定位線上BUG,超給力
  • 正則表達式在Java中的使用
  • 各國“乘地鐵法規”知多少,有的國家竟然那麽奇葩?
  • 程序員:單個TCP(Socket)連接,發送多個文件
  • JDK8新特性你都用了嗎?
  • 面試清單(Java崗)Java+JVM+數據庫+算法+Spring+中間件+設計模式
軍事

發佈留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

©2025 快讀 | 服務協議 | DMCA | 聯繫我們