Java 枚举

1、概览

enum关键字在 java5 中引入,表示一种特殊类型的类,其总是继承java.lang.Enum类,更多内容可以自行查看其官方文档。
枚举在很多时候会和常量拿来对比,可能因为本身大量实际使用枚举的地方就是为了替代常量。那么这种方式由什么优势呢?
以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录可接受值的列表,并避免由于传入无效值而引起的意外行为。
下面示例定义一个简单的枚举类型 pizza 订单的状态,共有三种 ORDERED, READY, DELIVERED状态:

  1. public enum PizzaStatus {
  2. ORDERED,
  3. READY,
  4. DELIVERED;
  5. }

简单来说,通过上面的代码避免了定义常量,将所有和 pizza 订单的状态的常量都统一放到了一个枚举类型里面。

  1. System.out.println(PizzaStatus.ORDERED.name());//ORDERED
  2. System.out.println(PizzaStatus.ORDERED);//ORDERED
  3. System.out.println(PizzaStatus.ORDERED.name().getClass());//class java.lang.String
  4. System.out.println(PizzaStatus.ORDERED.getClass());//class shuang.kou.enumdemo.enumtest.PizzaStatus

2、自定义枚举方法

现在对枚举是什么以及如何使用它们有了基本的了解,通过在枚举上定义一些额外的API方法,将上一个示例提升到一个新的水平:

  1. public class Pizza {
  2. private PizzaStatus status;
  3. publicenum PizzaStatus {
  4. ORDERED,
  5. READY,
  6. DELIVERED;
  7. }
  8. public boolean isDeliverable() {
  9. if (getStatus() == PizzaStatus.READY) {
  10. returntrue;
  11. }
  12. returnfalse;
  13. }
  14. // Methods that set and get the status variable.
  15. }

3、使用 == 比较枚举类型

由于枚举类型确保JVM中仅存在一个常量实例,因此可以安全地使用“ ==”运算符比较两个变量,如上例所示;此外,“ ==”运算符可提供编译时和运行时的安全性。
首先看一下以下代码段中的运行时安全性,其中“ ==”运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException:

  1. if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED));
  2. if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);

对于编译时安全性,看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为getStatus方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误:

  1. if(testPz.getStatus().equals(TestColor.GREEN));
  2. if(testPz.getStatus() == TestColor.GREEN);

4、在 switch 语句中使用枚举类型

  1. public int getDeliveryTimeInDays() {
  2. switch (status) {
  3. case ORDERED: return5;
  4. case READY: return2;
  5. case DELIVERED: return0;
  6. }
  7. return0;
  8. }

5、枚举类型的属性,方法和构造函数

可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。
下面,扩展上面的示例,实现从比萨的一个阶段到另一个阶段的过渡,并了解如何摆脱之前使用的if语句和switch语句:

  1. public class Pizza {
  2. private PizzaStatus status;
  3. publicenum PizzaStatus {
  4. ORDERED (5){
  5. @Override
  6. public boolean isOrdered() {
  7. returntrue;
  8. }
  9. },
  10. READY (2){
  11. @Override
  12. public boolean isReady() {
  13. returntrue;
  14. }
  15. },
  16. DELIVERED (0){
  17. @Override
  18. public boolean isDelivered() {
  19. returntrue;
  20. }
  21. };
  22. privateint timeToDelivery;
  23. public boolean isOrdered() {returnfalse;}
  24. public boolean isReady() {returnfalse;}
  25. public boolean isDelivered(){returnfalse;}
  26. public int getTimeToDelivery() {
  27. return timeToDelivery;
  28. }
  29. PizzaStatus (int timeToDelivery) {
  30. this.timeToDelivery = timeToDelivery;
  31. }
  32. }
  33. public boolean isDeliverable() {
  34. returnthis.status.isReady();
  35. }
  36. public void printTimeToDeliver() {
  37. System.out.println("Time to delivery is " +
  38. this.getStatus().getTimeToDelivery());
  39. }
  40. // Methods that set and get the status variable.
  41. }

下面这段代码展示它是如何 work 的:

  1. @Test
  2. public void givenPizaOrder_whenReady_thenDeliverable() {
  3. Pizza testPz = new Pizza();
  4. testPz.setStatus(Pizza.PizzaStatus.READY);
  5. assertTrue(testPz.isDeliverable());
  6. }

6、EnumSet and EnumMap

6.1. EnumSet

EnumSet 是一种专门为枚举类型所设计的 Set 类型。
HashSet相比,由于使用了内部位向量表示,因此它是特定 Enum 常量集的非常有效且紧凑的表示形式。
它提供了类型安全的替代方法,以替代传统的基于int的“位标志”,编写更易读和易于维护的简洁代码。
EnumSet 是抽象类,其有两个实现:RegularEnumSetJumboEnumSet,选择哪一个取决于实例化时枚举中常量的数量。
在很多场景中的枚举常量集合操作(如:取子集、增加、删除、containsAllremoveAll批操作)使用EnumSet非常合适;如果需要迭代所有可能的常量则使用Enum.values()

  1. public class Pizza {
  2. privatestatic EnumSet<PizzaStatus> undeliveredPizzaStatuses =
  3. EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);
  4. private PizzaStatus status;
  5. publicenum PizzaStatus {
  6. ...
  7. }
  8. public boolean isDeliverable() {
  9. returnthis.status.isReady();
  10. }
  11. public void printTimeToDeliver() {
  12. System.out.println("Time to delivery is " +
  13. this.getStatus().getTimeToDelivery() + " days");
  14. }
  15. public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
  16. return input.stream().filter(
  17. (s) -> undeliveredPizzaStatuses.contains(s.getStatus()))
  18. .collect(Collectors.toList());
  19. }
  20. public void deliver() {
  21. if (isDeliverable()) {
  22. PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
  23. .deliver(this);
  24. this.setStatus(PizzaStatus.DELIVERED);
  25. }
  26. }
  27. // Methods that set and get the status variable.
  28. }

下面的测试演示了展示了 EnumSet 在某些场景下的强大功能:

  1. @Test
  2. public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {
  3. List<Pizza> pzList = new ArrayList<>();
  4. Pizza pz1 = new Pizza();
  5. pz1.setStatus(Pizza.PizzaStatus.DELIVERED);
  6. Pizza pz2 = new Pizza();
  7. pz2.setStatus(Pizza.PizzaStatus.ORDERED);
  8. Pizza pz3 = new Pizza();
  9. pz3.setStatus(Pizza.PizzaStatus.ORDERED);
  10. Pizza pz4 = new Pizza();
  11. pz4.setStatus(Pizza.PizzaStatus.READY);
  12. pzList.add(pz1);
  13. pzList.add(pz2);
  14. pzList.add(pz3);
  15. pzList.add(pz4);
  16. List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);
  17. assertTrue(undeliveredPzs.size() == 3);
  18. }

6.2. EnumMap

EnumMap是一个专门化的映射实现,用于将枚举常量用作键。与对应的 HashMap 相比,它是一个高效紧凑的实现,并且在内部表示为一个数组:

  1. EnumMap<Pizza.PizzaStatus, Pizza> map;

快速看一个真实的示例,该示例演示如何在实践中使用它:

  1. public static EnumMap<PizzaStatus, List<Pizza>>
  2. groupPizzaByStatus(List<Pizza> pizzaList) {
  3. EnumMap<PizzaStatus, List<Pizza>> pzByStatus =
  4. new EnumMap<PizzaStatus, List<Pizza>>(PizzaStatus.class);
  5. for (Pizza pz : pizzaList) {
  6. PizzaStatus status = pz.getStatus();
  7. if (pzByStatus.containsKey(status)) {
  8. pzByStatus.get(status).add(pz);
  9. } else {
  10. List<Pizza> newPzList = new ArrayList<Pizza>();
  11. newPzList.add(pz);
  12. pzByStatus.put(status, newPzList);
  13. }
  14. }
  15. return pzByStatus;
  16. }

下面的测试演示了展示了 EnumMap 在某些场景下的强大功能:

  1. @Test
  2. public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
  3. List<Pizza> pzList = new ArrayList<>();
  4. Pizza pz1 = new Pizza();
  5. pz1.setStatus(Pizza.PizzaStatus.DELIVERED);
  6. Pizza pz2 = new Pizza();
  7. pz2.setStatus(Pizza.PizzaStatus.ORDERED);
  8. Pizza pz3 = new Pizza();
  9. pz3.setStatus(Pizza.PizzaStatus.ORDERED);
  10. Pizza pz4 = new Pizza();
  11. pz4.setStatus(Pizza.PizzaStatus.READY);
  12. pzList.add(pz1);
  13. pzList.add(pz2);
  14. pzList.add(pz3);
  15. pzList.add(pz4);
  16. EnumMap<Pizza.PizzaStatus,List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);
  17. assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);
  18. assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);
  19. assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);
  20. }

7、通过枚举实现一些设计模式

7.1 单例模式

通常,使用类实现 Singleton 模式并非易事,枚举提供了一种实现单例的简便方法。
《Effective Java 》和《Java与模式》都非常推荐这种方式,使用这种方式方式实现枚举可以有什么好处呢?

《Effective Java》 这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。—-《Effective Java 中文版 第二版》

《Java与模式》 《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

下面的代码段显示了如何使用枚举实现单例模式:

  1. publicenum PizzaDeliverySystemConfiguration {
  2. INSTANCE;
  3. PizzaDeliverySystemConfiguration() {
  4. // Initialization configuration which involves
  5. // overriding defaults like delivery strategy
  6. }
  7. private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;
  8. public static PizzaDeliverySystemConfiguration getInstance() {
  9. return INSTANCE;
  10. }
  11. public PizzaDeliveryStrategy getDeliveryStrategy() {
  12. return deliveryStrategy;
  13. }
  14. }

如何使用呢?请看下面的代码:

  1. PizzaDeliveryStrategy deliveryStrategy = PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy();

通过 PizzaDeliverySystemConfiguration.getInstance() 获取的就是单例的 PizzaDeliverySystemConfiguration

7.2 策略模式

通常,策略模式由不同类实现同一个接口来实现的。
这也就意味着添加新策略意味着添加新的实现类。使用枚举,可以轻松完成此任务,添加新的实现意味着只定义具有某个实现的另一个实例。
下面的代码段显示了如何使用枚举实现策略模式:

  1. publicenum PizzaDeliveryStrategy {
  2. EXPRESS {
  3. @Override
  4. public void deliver(Pizza pz) {
  5. System.out.println("Pizza will be delivered in express mode");
  6. }
  7. },
  8. NORMAL {
  9. @Override
  10. public void deliver(Pizza pz) {
  11. System.out.println("Pizza will be delivered in normal mode");
  12. }
  13. };
  14. public abstract void deliver(Pizza pz);
  15. }

给 Pizza增加下面的方法:

  1. public void deliver() {
  2. if (isDeliverable()) {
  3. PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
  4. .deliver(this);
  5. this.setStatus(PizzaStatus.DELIVERED);
  6. }
  7. }

如何使用呢?请看下面的代码:

  1. @Test
  2. public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {
  3. Pizza pz = new Pizza();
  4. pz.setStatus(Pizza.PizzaStatus.READY);
  5. pz.deliver();
  6. assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
  7. }

8、Java 8 与枚举

Pizza 类可以用Java 8重写,可以看到方法 lambda 和Stream API如何使 getAllUndeliveredPizzas()groupPizzaByStatus()方法变得如此简洁:

getAllUndeliveredPizzas()

  1. public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
  2. return input.stream().filter(
  3. (s) -> !deliveredPizzaStatuses.contains(s.getStatus()))
  4. .collect(Collectors.toList());
  5. }

groupPizzaByStatus()

  1. publicstatic EnumMap<PizzaStatus, List<Pizza>>
  2. groupPizzaByStatus(List<Pizza> pzList) {
  3. EnumMap<PizzaStatus, List<Pizza>> map = pzList.stream().collect(
  4. Collectors.groupingBy(Pizza::getStatus,
  5. () -> new EnumMap<>(PizzaStatus.class), Collectors.toList()));
  6. return map;
  7. }

9、Enum 类型的 JSON 表现形式

使用Jackson库,可以将枚举类型的JSON表示为POJO。下面的代码段显示了可以用于同一目的的Jackson批注:

  1. @JsonFormat(shape = JsonFormat.Shape.OBJECT)
  2. publicenum PizzaStatus {
  3. ORDERED (5){
  4. @Override
  5. public boolean isOrdered() {
  6. returntrue;
  7. }
  8. },
  9. READY (2){
  10. @Override
  11. public boolean isReady() {
  12. returntrue;
  13. }
  14. },
  15. DELIVERED (0){
  16. @Override
  17. public boolean isDelivered() {
  18. returntrue;
  19. }
  20. };
  21. privateint timeToDelivery;
  22. public boolean isOrdered() {returnfalse;}
  23. public boolean isReady() {returnfalse;}
  24. public boolean isDelivered(){returnfalse;}
  25. @JsonProperty("timeToDelivery")
  26. public int getTimeToDelivery() {
  27. return timeToDelivery;
  28. }
  29. private PizzaStatus (int timeToDelivery) {
  30. this.timeToDelivery = timeToDelivery;
  31. }
  32. }

可以按如下方式使用 Pizza 和 PizzaStatus:

  1. Pizza pz = new Pizza();
  2. pz.setStatus(Pizza.PizzaStatus.READY);
  3. System.out.println(Pizza.getJsonString(pz));

生成 Pizza 状态以以下JSON展示:

  1. {
  2. "status" : {
  3. "timeToDelivery" : 2,
  4. "ready" : true,
  5. "ordered" : false,
  6. "delivered" : false
  7. },
  8. "deliverable" : true
  9. }

有关枚举类型的JSON序列化/反序列化(包括自定义)的更多信息,请参阅Jackson-将枚举序列化为JSON对象。

10、总结

讨论了Java枚举类型,从基础知识到高级应用以及实际应用场景,感受到枚举的强大功能。

11、补充

在上面讲到了可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。
下面通过一个实际的例子展示一下,当调用短信验证码的时候可能有几种不同的用途,在下面这样定义:

  1. publicenum PinType {
  2. REGISTER(100000, "注册使用"),
  3. FORGET_PASSWORD(100001, "忘记密码使用"),
  4. UPDATE_PHONE_NUMBER(100002, "更新手机号码使用");
  5. privatefinalint code;
  6. privatefinal String message;
  7. PinType(int code, String message) {
  8. this.code = code;
  9. this.message = message;
  10. }
  11. public int getCode() {
  12. return code;
  13. }
  14. public String getMessage() {
  15. return message;
  16. }
  17. @Override
  18. public String toString() {
  19. return"PinType{" +
  20. "code=" + code +
  21. ", message='" + message + '\'' +
  22. '}';
  23. }
  24. }

实际使用:

  1. System.out.println(PinType.FORGET_PASSWORD.getCode());
  2. System.out.println(PinType.FORGET_PASSWORD.getMessage());
  3. System.out.println(PinType.FORGET_PASSWORD.toString());

Output:

  1. 100001
  2. 忘记密码使用
  3. PinType{code=100001, message='忘记密码使用'}

这样的话,在实际使用起来就会非常灵活方便!