Java 枚举使用
概述
在本文中,我们将会了解到什么是 Java 枚举,枚举解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式。
enum 这个关键字出现在 Java5 中,它继承 java.lang.Enum 类,有关枚举更多的内容,请参考 官方文档。
以这种方式定义的常量使得代码可读性更高,允许编译时异常检查,预先记录可接受值的列表,避免由于传入无效的值引发意外情况。
下面的代码定义了一个简单快捷的枚举示例,它定义了 pizza 订单的三种状态:ORDERED、READY 、DELIVERED。
public enum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}
另外,枚举还有好多有用的方法。如果你用传统的 public static final 常量,这些方法是需要你手动写的。
自定义方法
目前为止,我们对枚举是什么以及如何使用有了基本了解。让我们在之前的例子基础上再定义一些额外的方法,让我们之前的例子更好:
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.
}
使用 == 运算符比较枚举类型
由于枚举类型能够确保 JVM 中仅存在一个常量的实例,因此我们可以在上述例子中使用 ==
运算符来比较两个变量。此外,==
运算符可提供编译时以及运行时的安全性。
让我们首先看下一下代码的运行时安全。其中 ==
运算符用于比较两个状态,并且如果两个值均为 null 都不会引发 NullPointerException。相反,如果使用 equals() 方法,则会抛出 NullPointerException 的异常。
if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED));
if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);
至于编译时安全,我们看另外一个例子,使用 equals() 方法比较结果为 true,因为 getStatus() 方法的枚举值和另一个和另一个枚举值相同,但逻辑上应该为 false。这个问题可以通过使用 ==
运算符避免。
编译器会显示类型不兼容错误:
if(testPz.getStatus().equals(TestColor.GREEN));
if(testPz.getStatus() == TestColor.GREEN);
在 switch 语句中使用枚举类型
枚举类型同样可以被用在 switch 代码块中:
public int getDeliveryTimeInDays() {
switch (status) {
case ORDERED: return 5;
case READY: return 2;
case DELIVERED: return 0;
}
return 0;
}
枚举类型中的属性,方法,构造器
你可以在枚举类型中定义属性、方法、构造器让它功能更强大。
让我们接着拓展上面的示例,实现 pizza 从一个阶段到另一个阶段的过渡,并了解如何摆脱之前的 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());
}
EnumSet 和 EnumMap
EnumSet
EnumSet
是一种专门为枚举设计的 Set
类型。
由于使用了内部位向量表示,相对于 HashSet
来说,EnumSet
是特定的 Enum
常量集的有效紧凑的表示方式。并且它为传统的基于 int 的位标志提供了类型类型安全的替代方案,使我们能够编写更易读和易于维护的简洁代码。
EnumSet
是抽象类,有两个实现:RegularEnumSet
以及 JumboEnumSet
。使用哪一个取决于实例化时枚举中常量的数量。
因此在我们想操作(取子集、添加、删除、containsAll 以及 removeAll)枚举常量集合的大部分情况下,使用 EnumSet
非常合适,如果仅仅需要遍历所有常量的值可以使用 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);
}
EnumMap
EnumMap
是 Map
的一种特殊实现,用来将枚举常量用作键。与对应的 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;
}
执行下面的测试代码证明了实现 Map
接口的 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);
}
用枚举实现设计模式
单例模式
通常,使用类实现单例模式是不容易的,枚举提供了一种简单快速的方式实现枚举。
另外,由于枚举类在底层实现了 Serializable
接口,所以 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;
}
}
策略模式
通常,策略模式是通过具有由不同类实现的接口来编写的。
添加一个新的策略意味着添加一个新的实现类。有了枚举,可以很容易的达成这个目的。添加一个新的实现类意味着仅仅定义具有某种实现的另一个实例。
下面的代码展示了如何使用枚举实现策略模式:
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);
}
Java 8 与枚举
Pizza 类可以使用 Java 8 重写,你可以看到用了 Java 8 中的 Lambda 和 Stream 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;
}
枚举类型的 JSON 表现形式
使用了 Jackson 库后,可以将枚举类型的 JSON 表示为 POJOs,下面的代码段展示了可以用于上述目的的 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 和 PizzaStatus:
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
System.out.println(Pizza.getJsonString(pz));
Pizzas 状态的 JSON 表现形式:
{
"status" : {
"timeToDelivery" : 2,
"ready" : true,
"ordered" : false,
"delivered" : false
},
"deliverable" : true
}
有关枚举类型的 JSON 序列化以及反序列化,请参考 Jackson-将枚举序列化为 JSON 对象
总结
在这篇文章中,我们从基础知识、高级用法以及实际场景分别探索了 Java 中的枚举使用。这篇文章的源码可以从 Github 下载。
上述内容翻译自 A Guide to Java Enums
推荐阅读
深度分析Java的枚举类型—-枚举的线程安全性及序列化问题
还没有评论,来说两句吧...