1. 概述


在这篇文章中,我们将看到什么是Java 枚举,它解决了什么问题,以及它们在实践中的一些设计模式。

Java 5中引入了"enum"关键字。它表示一种特殊类型的类,该类始终扩展自java.lang.Enum类。有关其使用情况的官方文档,请查看​​文档​​。


这样定义的常量使代码更具可读性,允许编译时检查,预先记录接受值的列表,避免由于传递无效值而导致的意外。

下面是一个快速而简单的示例,用于定义比萨饼订单的状态;订单状态可以订购就绪交付


public enum PizzaStatus {     ORDERED,//已订购     READY, //已准备     DELIVERED; //已交付 }


此外,它们还具有许多有用的方法,如果您使用传统的公共静态最终常量(public static final constants),你必须自己编写这些方法。

2. 自定义分分方法

好了,现在我们基本了解了什么是项级以及如何使用它们,让我们通过定义一些额外的 API 方法,将前面的示例放在下一个级别:


public class Pizza {     private PizzaStatus status;     public enum PizzaStatus {         ORDERED,         READY,         DELIVERED;     }       public boolean isDeliverable() {         if (getStatus() == PizzaStatus.READY) {             return true;         }         return false;     }          // Methods that set and get the status variable. }



3. 使用"=="运算符比较枚举类型

由于 enum 类型确保 JVM 中只存在一个常量实例,因此我们可以如上安全地使用"=="运算符比较两个变量,"=="运算符还提供编译时和运行时安全。

让我们首先看运行时安全,其中"=="运算符用于比较状态,如果任一值为 null ,则不会引发NullPointerException 。 相反,如果使用等值方法(equals),将引发 NullPointerException:


if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); //may null


if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);//not null exception

至于编译时安全,让我们看另一个示例,其中equals 方法比较不同类型的值可能是true - 因为枚举值与getStatus 方法的值恰好一样,但从逻辑上讲,这个比较是错误的(类型不同应该是false)。使用"=="可避免此问题。

编译器将比较标记为不兼容错误:


if(testPz.getStatus().equals(TestColor.GREEN));


if(testPz.getStatus() == TestColor.GREEN);

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


Enum 类型也可用于switch语句:


public int getDeliveryTimeInDays() {


switch (status) {


case ORDERED: return 5;


case READY: return 2;


case DELIVERED: return 0;


}


return 0;


}

5. 在字段、方法和构造函数中的枚举


您可以在构造函数、方法和字段中定义枚举,使其非常强大。


让我们扩展上面的示例,实现披萨状态的过渡,并看看我们如何摆脱之前使用的 if语句和 switch语句:


public class Pizza {




private PizzaStatus status;


public enum PizzaStatus {


ORDERED (5){


@Override


public boolean isOrdered() {


return true;


}


},


READY (2){


@Override


public boolean isReady() {


return true;


}


},


DELIVERED (0){


@Override


public boolean isDelivered() {


return true;


}


};




private int timeToDelivery;




public boolean isOrdered() {return false;}




public boolean isReady() {return false;}




public boolean isDelivered(){return false;}




public int getTimeToDelivery() {


return timeToDelivery;


}




PizzaStatus (int timeToDelivery) {


this.timeToDelivery = timeToDelivery;


}


}




public boolean isDeliverable() {


return this.status.isReady();


}




public void printTimeToDeliver() {


System.out.println("Time to delivery is " +


this.getStatus().getTimeToDelivery());


}




// Methods that set and get the status variable.


}

下面的测试片段演示了它是如何工作的:


@Test


public void givenPizaOrder_whenReady_thenDeliverable() {


Pizza testPz = new Pizza();


testPz.setStatus(Pizza.PizzaStatus.READY);


assertTrue(testPz.isDeliverable());


}

6.枚举集(EnumSet)和枚举图(EnumMap)

6.1.EnumSet

EnumSet 是专用的 Set实现,用于与 Enum 类型一起使用。

与哈希集相比,由于内部使用了位矢量(internal Bit Vector)表示,因此它是高效紧凑的 Enum常数表示形式。它安全地替代了传统基于int 形的位标记 “bit flags”,使我们能够编写易读易维护的代码。

EnumSet是一个抽象类,它有两个实现:"RegularEnumSet""JumboEnumSet",它根据实例化时依赖的常量数来选择。

因此我们最好在需要集合常量时使用EnumSet(如子设置、添加、删除和批量操作(如"包含 All""删除全部")以及使用 Enum.values ()

在下面的代码展示如何使用EnumSet创建常量子集:


public class Pizza {




private static EnumSet<PizzaStatus> undeliveredPizzaStatuses =


EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);




private PizzaStatus status;




public enum PizzaStatus {


...


}




public boolean isDeliverable() {


return this.status.isReady();


}




public void printTimeToDeliver() {


System.out.println("Time to delivery is " +


this.getStatus().getTimeToDelivery() + " days");


}




public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {


return input.stream().filter(


(s) -> undeliveredPizzaStatuses.contains(s.getStatus()))


.collect(Collectors.toList());


}




public void deliver() {


if (isDeliverable()) {


PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()


.deliver(this);


this.setStatus(PizzaStatus.DELIVERED);


}


}




// Methods that set and get the status variable.


}

执行以下测试演示了 Set 接口的EnumSet实现功能


@Test


public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {


List<Pizza> pzList = new ArrayList<>();


Pizza pz1 = new Pizza();


pz1.setStatus(Pizza.PizzaStatus.DELIVERED);




Pizza pz2 = new Pizza();


pz2.setStatus(Pizza.PizzaStatus.ORDERED);




Pizza pz3 = new Pizza();


pz3.setStatus(Pizza.PizzaStatus.ORDERED);




Pizza pz4 = new Pizza();


pz4.setStatus(Pizza.PizzaStatus.READY);




pzList.add(pz1);


pzList.add(pz2);


pzList.add(pz3);


pzList.add(pz4);




List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);


assertTrue(undeliveredPzs.size() == 3);


}

6.2. EnumMap


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


EnumMap<Pizza.PizzaStatus, Pizza> map;

让我们快速了解一个真实示例,说明如何在实践中使用它:


public static EnumMap<PizzaStatus, List<Pizza>>


groupPizzaByStatus(List<Pizza> pizzaList) {


EnumMap<PizzaStatus, List<Pizza>> pzByStatus =


new EnumMap<PizzaStatus, List<Pizza>>(PizzaStatus.class);




for (Pizza pz : pizzaList) {


PizzaStatus status = pz.getStatus();


if (pzByStatus.containsKey(status)) {


pzByStatus.get(status).add(pz);


} else {


List<Pizza> newPzList = new ArrayList<Pizza>();


newPzList.add(pz);


pzByStatus.put(status, newPzList);


}


}


return pzByStatus;


}

执行以下测试演示了映射接口的 EnumMap实现功能



@Test


public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {


List<Pizza> pzList = new ArrayList<>();


Pizza pz1 = new Pizza();


pz1.setStatus(Pizza.PizzaStatus.DELIVERED);




Pizza pz2 = new Pizza();


pz2.setStatus(Pizza.PizzaStatus.ORDERED);




Pizza pz3 = new Pizza();


pz3.setStatus(Pizza.PizzaStatus.ORDERED);




Pizza pz4 = new Pizza();


pz4.setStatus(Pizza.PizzaStatus.READY);




pzList.add(pz1);


pzList.add(pz2);


pzList.add(pz3);


pzList.add(pz4);




EnumMap<Pizza.PizzaStatus,List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);


assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);


assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);


assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);


}

7. 使用枚举实现设计模式

7.1. 单例模式

通常,单例模式实现是很简单的。Enums 提供了一种更快速的方法。

此外,由于 enum 类在保护模式下实现可序列化接口,因此 JVM 保证该类是单例,这与常规实现不同,在非序列化期间,我们必须确保不创建任何新实例。

在下面的代码片段中,我们将了解如何实现单例模式:


public enum PizzaDeliverySystemConfiguration {


INSTANCE;


PizzaDeliverySystemConfiguration() {


// Initialization configuration which involves


// overriding defaults like delivery strategy


}




private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;




public static PizzaDeliverySystemConfiguration getInstance() {


return INSTANCE;


}




public PizzaDeliveryStrategy getDeliveryStrategy() {


return deliveryStrategy;


}


}

7.2. 战略模式


通常策略模式具有不同类实现的接口编写的。

添加新策略意味着添加新的实现类。使用枚举只需少量工作,添加新实现意味着只需实现定义方法即可。

下面的代码段显示了如何实现策略模式:


public enum PizzaDeliveryStrategy {


EXPRESS {


@Override


public void deliver(Pizza pz) {


System.out.println("Pizza will be delivered in express mode");


}


},


NORMAL {


@Override


public void deliver(Pizza pz) {


System.out.println("Pizza will be delivered in normal mode");


}


};




public abstract void deliver(Pizza pz);


}

将以下方法添加到Pizza类:


public void deliver() {


if (isDeliverable()) {


PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()


.deliver(this);


this.setStatus(PizzaStatus.DELIVERED);


}


}


@Test


public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {


Pizza pz = new Pizza();


pz.setStatus(Pizza.PizzaStatus.READY);


pz.deliver();


assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);


}

8. Java 8 和 enum


在 Java 8 中可以重写 Pizza 类,您可以看到使用 lambdas和流API的方法getAllUndeliveredPizzas()和groupPizzaByStatus()变得如此简洁:


public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {


return input.stream().filter(


(s) -> !deliveredPizzaStatuses.contains(s.getStatus()))


.collect(Collectors.toList());


}




public static EnumMap<PizzaStatus, List<Pizza>>


groupPizzaByStatus(List<Pizza> pzList) {


EnumMap<PizzaStatus, List<Pizza>> map = pzList.stream().collect(


Collectors.groupingBy(Pizza::getStatus,


() -> new EnumMap<>(PizzaStatus.class), Collectors.toList()));


return map;


}

9. Enum的JSON 描述

使用 Jackson 库,可以像 POJOs 一样具有 JSON 表示方式。下面的代码段显示了可用于相同的 Jackson 注释:


@JsonFormat(shape = JsonFormat.Shape.OBJECT)


public enum PizzaStatus {


ORDERED (5){


@Override


public boolean isOrdered() {


return true;


}


},


READY (2){


@Override


public boolean isReady() {


return true;


}


},


DELIVERED (0){


@Override


public boolean isDelivered() {


return true;


}


};




private int timeToDelivery;




public boolean isOrdered() {return false;}




public boolean isReady() {return false;}




public boolean isDelivered(){return false;}




@JsonProperty("timeToDelivery")


public int getTimeToDelivery() {


return timeToDelivery;


}




private PizzaStatus (int timeToDelivery) {


this.timeToDelivery = timeToDelivery;


}


}

我们可以使用披萨和披萨统计:


Pizza pz = new Pizza();


pz.setStatus(Pizza.PizzaStatus.READY);


System.out.println(Pizza.getJsonString(pz));

生成披萨状态的 JSON表示形式



{


"status" : {


"timeToDelivery" : 2,


"ready" : true,


"ordered" : false,


"delivered" : false


},


"deliverable" : true


}

有关亿万类的 JSON 序列化/去序列化(包括自定义)的信息,​​请参阅 Jackson 序列化 作为 JSON 对象​​。

10. 结论

在这篇文章中,我们探讨了Java的枚举,从语言基础知识到更高级和更有趣的实际用例。本文中的代码段可以在​​Github 存储库中​​找到。