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 存储库中找到。